An authenticated SSH denial of service in SFTPGo.
During a client engagement, we identified GHSA-q7pc-356p-hggc, affecting SFTPGo through 2.7.4 and fixed in 2.7.5.
The client relied on a managed SFTP service for file transfers, and part of our work was to understand the security posture of that dependency. The service identified itself as SFTPGo, so we mapped the software to public advisories and read the relevant request-handling code.
The SSH subsystem path contained an unchecked slice. A valid account could reach it with a malformed request, causing a panic that terminated the SFTPGo process.
Root cause: an unchecked slice
After authentication, an SSH client opens a session channel and asks the server to start a subsystem. For SFTP, the subsystem name is sftp. In SFTPGo 2.7.4, the request handler checked it like this:
case "subsystem":
if bytes.Equal(req.Payload[4:], []byte("sftp")) {
// start the SFTP connection
}
An SSH string is encoded as a four-byte length followed by that many bytes of data. A normal SFTP subsystem request therefore looks like this:
00 00 00 04 73 66 74 70
└─ length ─┘ s f t p
The code skipped the length field and compared the remaining bytes with sftp. What it did not do was first establish that four bytes existed. Because the payload comes from the authenticated client, a client could send zero, one, two, or three bytes instead.
At three bytes, req.Payload[4:] is outside the slice bounds. The Go runtime raises a panic:
panic: runtime error: slice bounds out of range [4:3]
github.com/drakkan/sftpgo/v2/internal/sftpd.
(*Configuration).AcceptInboundConnection.func2(...)
internal/sftpd/server.go:694
Why the panic terminated the process
The vulnerable code ran inside a goroutine created for channel requests. SFTPGo had recovery logic elsewhere, including around the SFTP connection handler, but Go's recover only catches a panic in the same goroutine. Those recovery boundaries could not reach this one.
An unrecovered panic in any goroutine terminates the Go program. In our single-process lab instance, the malformed request terminated SFTPGo, dropped both test sessions, and left the service unavailable until the container restarted.
Authentication established who the user was; it did not make the user's SSH framing trustworthy. The subsystem payload still required validation after login.
Controlled validation
We built SFTPGo 2.7.4 in a disposable lab and created two ordinary users. One maintained a live SFTP session while the other authenticated, opened a session channel, and sent a malformed subsystem request. The relevant part of the reproducer was:
ch, requests, err := client.OpenChannel("session", nil)
if err != nil {
return err
}
go ssh.DiscardRequests(requests)
// Three bytes are shorter than the SSH string length prefix.
_, _ = ch.SendRequest("subsystem", false, []byte{1, 2, 3})
The SFTPGo container exited with status 2 and the second user's connection was lost immediately. Payload lengths from zero through three reproduced the crash. Four-byte and longer payloads did not trigger this specific bounds panic, although malformed messages of those lengths still needed to be rejected correctly.
With automatic restart enabled, each crash produced a short outage. Repeating the request after each restart kept the lab service intermittently unavailable.
Impact and constraints
This was an authenticated remote denial of service. It was not an authentication bypass, data disclosure, privilege escalation, or remote code execution issue.
- Access required: a valid account able to authenticate to the SSH/SFTP service and open a session channel.
- Permissions required after login: none beyond reaching the channel handler; file write access was not required.
- Blast radius: the affected SFTPGo process and the connections it handled. Other nodes in a high-availability deployment were not tested.
- Observed trigger: one short subsystem request after authentication.
- Public rating: Medium, CVSS 3.1 score 6.5 (
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).
In the lab, the second user's live session dropped when the process terminated. The same failure mode can interrupt other users or scheduled integrations handled by that process.
The vendor fix
Our first suggested patch was a minimum-length check. The SFTPGo maintainer instead parsed the payload as the SSH wire-format structure before comparing the decoded subsystem name.
case "subsystem":
var msg sshSubsystemMsg
if err := ssh.Unmarshal(req.Payload, &msg); err == nil && msg.Name == "sftp" {
// start the SFTP connection
}
This removes the unsafe slice and rejects short or malformed payloads through the protocol parser. The patch also added regression coverage for empty, short, malformed, and non-SFTP subsystem requests, followed by a normal SFTP connection to prove the server remained alive.
The fix landed in commit 4f751232 and was released in SFTPGo 2.7.5. The advisory lists all versions through 2.7.4 as affected.
Disclosure timeline
- 30 June 2026: identified during a client engagement, reproduced in a controlled lab, and privately reported to the SFTPGo maintainer with reproduction details and a proposed minimum-length check.
- 17 July 2026: the SFTPGo maintainer committed the parser-based fix and regression tests.
- 17 July 2026: SFTPGo 2.7.5 and GHSA-q7pc-356p-hggc were published.
- 21 July 2026: this technical write-up was published after users had a patched release available.
Thank you to Nicola Murino and the SFTPGo project for coordinating the disclosure and patched release.
What operators should do
Upgrade SFTPGo to 2.7.5 or later. If an immediate upgrade is not possible, restrict access to trusted SFTP accounts and networks, monitor for unexpected process exits, and ensure restart behaviour is visible to operators. Process supervision and high availability can reduce outage time, but neither substitutes for patching because an authenticated client can repeat the trigger.
For maintainers, authentication does not remove the need to validate protocol fields. Parse structured wire data with the protocol library instead of indexing into raw payloads, and add regression cases for malformed messages.
References
- GHSA-q7pc-356p-hggc: Improper handling of malformed SSH channel requests
- SFTPGo patch commit and regression tests
- SFTPGo 2.7.5 release notes
What we took from the engagement
This finding came from following a dependency question from service identification, to version exposure, to source review, and finally to controlled validation. For externally managed components, mapping a version banner to published advisories can be the start of the review rather than the end.