Skip to content

fix(sftp): sshj/Cyberduck clients dropped on initial REALPATH/STAT via russh strict-parser SshEncoding::Length on incoming decode #215

Description

@Yaminyam

Summary

sshj/Java-based SFTP clients (Cyberduck, and likely others) cannot list any directory against bssh-server. Right after the SFTP handshake, on the client's initial SSH_FXP_REALPATH (type 16) + SSH_FXP_STAT (type 17) of the home directory, the server drops the connection with Session error error=SshEncoding: length invalid. The OpenSSH sftp CLI and FileZilla work against the exact same server, config, user, and directory. Reproduces with bssh-server v2.2.3.

Root Cause (corrects the original hypothesis)

The original report supposed the server mis-encodes the outgoing REALPATH/STAT response (SSH_FXP_NAME / SSH_FXP_ATTRS). A code audit shows that is not the cause:

  • SshEncoding: length invalid is not produced anywhere in the SFTP crate. It is russh::Error::SshEncoding(ssh_encoding::Error::Length) (crates/bssh-russh/src/lib_inner.rs:232-233, rendered as "length invalid"), surfaced by bssh at src/server/mod.rs:369-374. It is raised only through the map_err!(...)? calls in russh's incoming packet parser (crates/bssh-russh/src/server/encrypted.rs).
  • The SFTP NAME/ATTRS encoders (crates/bssh-russh-sftp/src/protocol/{name,attrs,file,file_attrs}.rs, ser.rs) write byte-correct u32 length prefixes, and the outgoing russh channel framing (crates/bssh-russh/src/session.rs:441-500) is correct. Crucially, the realpath/stat handler output (src/server/sftp.rs:983-1128) is client-independent: an outgoing-encode bug would break OpenSSH and FileZilla too, which contradicts the observation that they work.

So the error is an incoming russh decode: ssh_encoding::Error::Length fires when a length-prefixed field (String::decode / Bytes::decode / u32::decode) declares more bytes than remain in the packet reader, i.e. russh's (now strict) parser reads a field at an offset where sshj's byte layout does not match what russh expects. The most likely site is the CHANNEL_DATA handler's Bytes::decode at crates/bssh-russh/src/server/encrypted.rs:842, but any strict handler in that file qualifies (CHANNEL_REQUEST field reads :944-1183, or an sshj-initiated rekey KEXINIT hitting the name-list limits in crates/bssh-russh/src/helpers.rs:38-40,96-97).

Likely regression window: commit 3bcd56f ("update: sync russh/russh-sftp forks to upstream and unify ssh-key (#207)") introduced strict fixed-layout parsing plus ensure_end (crates/bssh-russh/src/parsing.rs:13-22) and the LimitedString/NameList limits, and touched encrypted.rs, helpers.rs, the whole SFTP crate, and src/server/sftp.rs. (Note: ensure_end returns TrailingData, not Length, so the specific length invalid points at a *::decode overrun, not trailing bytes.)

Proposed Solution

  1. Identify the exact overrun site (runtime evidence, static analysis cannot see sshj's bytes). Temporarily log the message id byte and raw payload length just before the match msg in server_read_authenticated (crates/bssh-russh/src/server/encrypted.rs, near :803), or instrument each map_err!(…decode…)? site, then reproduce with Cyberduck/sshj to capture which decode returns Err(Length) and on which SSH message.
  2. Fix the parser to accept the sshj-legal message. Depending on the finding: relax an over-strict fixed-layout assumption, correctly consume an optional/variable field sshj includes, or raise/adjust a limit if a rekey name-list is the trigger. Preserve the hardening intent of #207 for genuinely malformed packets while accepting well-formed sshj traffic.
  3. Add an interop regression test exercising the REALPATH+STAT-then-OPENDIR sequence an sshj client sends, so this class of client stays covered.

Implementation Notes

  • Error definition and surfacing: crates/bssh-russh/src/lib_inner.rs:232-233; src/server/mod.rs:369-374; bssh's SshHandler::Error = anyhow::Error (src/server/handler.rs:250-251) preserves the russh Display.
  • Incoming parser (fault domain): crates/bssh-russh/src/server/encrypted.rs:797-1365; explicit Error::Length sites at crates/bssh-russh/src/helpers.rs:38-40,96-97; ensure_end at crates/bssh-russh/src/parsing.rs:13-22.
  • Outgoing path (ruled out): crates/bssh-russh/src/session.rs:441-500; SFTP encoders under crates/bssh-russh-sftp/src/protocol/ and ser.rs.
  • The SFTP request parser is tolerant (each SFTP packet is independently length-framed in crates/bssh-russh-sftp/src/utils.rs:12-25, and the SFTP stream is decoupled from SSH-packet decoding), so trailing bytes in an SFTP-level REALPATH request cannot cause this; the fault is at the SSH transport layer.
  • Reproduction harness: a Java sshj snippet (or Cyberduck) doing connect → REALPATH(".") → STAT is enough to trigger it deterministically; pair it with the instrumentation in step 1.

Acceptance Criteria

  • The exact map_err!(…decode…)? site and the offending SSH message from sshj are identified and documented.
  • Cyberduck (and a raw sshj client) can connect, list, and transfer against bssh-server without the connection being dropped.
  • OpenSSH sftp and FileZilla continue to work (no interop regression), and the #207 hardening remains effective against genuinely malformed packets.
  • An automated interop/regression test covers the sshj REALPATH/STAT/OPENDIR sequence.

Related


Original Suggestion

Title: SFTP: Cyberduck (sshj) connection dropped on initial REALPATH/STAT — server errors SshEncoding: length invalid

Summary

Cyberduck (and likely other sshj/Java-based SFTP clients) cannot list any directory against bssh-server: right after the SFTP handshake, on the client's initial REALPATH + STAT of the home directory, the server drops the connection with:

Session error  error=SshEncoding: length invalid

The OpenSSH sftp CLI and FileZilla work fine against the exact same server, config, user, and directory — so this is specific to the request/response pattern that sshj-based clients use.

Client-side symptom (Cyberduck)

디렉터리 sion의 목록을 얻어오는데 실패하였습니다. (127.0.0.1 – SFTP)
Broken transport; encountered EOF. The connection attempt was rejected.
The server may be down, or your network may not be properly configured.

Server-side log (bssh-server run -vv)

INFO  bssh::server::handler: Starting SFTP session user=sion peer=127.0.0.1:54766 home=/Users/sion
DEBUG bssh::server::sftp: Creating SFTP handler user=sion chroot=None cwd=/Users/sion
INFO  bssh::server::sftp: SFTP session initialized user=sion version=3
DEBUG bssh_russh_sftp::protocol: packet type 16      # SSH_FXP_REALPATH
DEBUG bssh_russh_sftp::protocol: packet type 17      # SSH_FXP_STAT
DEBUG russh::server: Connection closed with error
DEBUG bssh_russh_sftp::server: sftp stream ended
ERROR bssh::server: Session error error=SshEncoding: length invalid

The connection dies immediately after the REALPATH (16) / STAT (17) exchange — before any OPENDIR/READDIR. So the server appears to mis-encode the REALPATH/STAT response (an SSH_FXP_NAME / SSH_FXP_ATTRS packet) such that the outgoing SSH channel-data length is invalid, and russh's transport encoder aborts the connection.

Repro

  1. bssh-server run -c config.yaml -D (no chroot: sftp.root unset; auth publickey or password; SFTP protocol v3 negotiated).
  2. Connect with Cyberduck (SFTP, 127.0.0.1, the configured user) → fails as above.
  3. Connect with OpenSSH sftp or FileZilla to the same server → works (list, get, put, rename, etc. all fine).

Impact

Many GUI/mobile SFTP clients are sshj/JSch-based (Cyberduck, and others). If they all hit this, bssh-server cannot be a drop-in SFTP endpoint for those clients.

Environment

  • bssh-server 2.2.3 (bssh-server-macos-aarch64)
  • SFTP protocol version 3
  • Failing client: Cyberduck (sshj); working clients: OpenSSH sftp, FileZilla

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions