You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: SCP/SFTP path resolution and dead chroot config (#186) (#194)
* feat: add scp.root config and thread sftp/scp chroot through ServerConfig
Previously `SftpConfig.root` was declared but never read, and `ScpConfig`
had no `root` field at all. Both handler-construction sites in `SshHandler`
hard-coded `user_info.home_dir` as the chroot root, so the configured
chroot setting was dead code.
This commit adds `ScpConfig.root: Option<PathBuf>`, plus `sftp_root` and
`scp_root` on the builder-based `ServerConfig`, and threads them through
`into_server_config()`. SCP falls back to `sftp.root` when `scp.root` is
unset, so a single top-level chroot setting governs both subsystems unless
admins explicitly want them split.
Also adds focused unit tests covering the precedence rules and the
no-chroot default. The handler-side wiring lands in a follow-up commit.
Refs #186
* fix: stop SCP/SFTP path doubling and honor target_is_directory
Three related defects in bssh-server's file-transfer subsystems caused
SCP and SFTP uploads to fail when the client supplied an absolute
destination path. Reported in #186 against the published v2.1.1 build.
1. Path doubling. `ScpHandler::resolve_path` and
`SftpHandler::resolve_path_static` unconditionally re-rooted every
absolute client path under the user's home directory, so
`scp local user@host:/home/work/file.bin` against root `/home/work`
produced `/home/work/home/work/file.bin`. The same bug broke
`bssh upload local /abs/remote.bin` with `No such file`.
Both resolvers now carry an `Option<PathBuf>` chroot root and a
separate `cwd` for relative paths. With no chroot (the new default),
absolute paths are honored verbatim and relative paths resolve from
the user's home directory, matching OpenSSH `sftp-server`/`scp`. With
chroot, absolute paths inside the chroot are honored verbatim (no
doubling); absolute paths outside are rejected with
`permission_denied`. Plain `/` keeps mapping to the chroot directory
so the `realpath` roundtrip used by interactive SFTP clients still
works. Path-traversal via `..` is still clamped to the chroot, and
the existing symlink-escape canonicalization is preserved.
2. SCP filename appending. `receive_file` always appended the source
filename to the resolved target, so `scp local.bin host:/tmp/dest.bin`
wrote to `/tmp/dest.bin/local.bin`. The handler now consults
`target_is_directory` (parsed from `-d`/`-r`) and the filesystem
state of the resolved target. Single-file destinations write
directly to the resolved path; directory destinations (existing
directory, `-d`/`-r` flag, or recursive descent) keep the
filename-appending behavior.
3. Dead-code chroot config. `SshHandler` now reads `config.sftp_root`
and `config.scp_root` and threads them into the handlers, so
setting `sftp.root` in the YAML actually changes the chroot.
Behavior change: with no `sftp.root`/`scp.root` configured, the server
no longer applies an implicit chroot at the user's home directory.
Deployments that intentionally wanted that confinement must now set
the field explicitly. This matches OpenSSH defaults and is the
recommended setup for Backend.AI session containers per the issue.
Closes#186
* docs: cover new chroot semantics and add issue #186 integration tests
Document the no-chroot default, the chroot semantics (no path doubling
inside, rejection outside, `..` clamped at the boundary), and the
`scp.root` field that falls back to `sftp.root`. Update the security
guide and the server-configuration architecture doc accordingly, and
add the migration note to the changelog.
Add `tests/scp_sftp_path_resolution_test.rs` covering the Backend.AI
reproduction scenarios (absolute SCP/SFTP paths, no doubling, no chroot
honors absolute paths, chroot rejects out-of-root paths, `..` clamped,
chroot `/` round-trips through `realpath`) plus symlink-escape blocking
under chroot. End-to-end tests against a running `bssh-server` with
real `scp`/`bssh upload` clients are out of scope for the Rust harness
but the path-resolution layer where the bugs lived is covered here.
Refs #186
* fix(security): block chroot bypass via parent-directory symlinks
The chroot resolver only verified lexical containment for paths whose
final component did not exist (the typical create/mkdir flow). A symlink
inside the chroot pointing to a directory outside the chroot let a
client target chroot/escape/newfile and have OpenOptions::open() or
create_dir() follow the symlink to operate outside the chroot.
ScpHandler::resolve_path and SftpHandler::resolve_path_static now walk
up to the closest existing ancestor of the target path, canonicalize
both that ancestor and the chroot root, and verify the canonical
ancestor stays inside the canonical root. Operator-misconfigured chroots
that don't exist on disk fall back to the lexical check.
Found during PR #194 review. Adds 6 regression tests covering both SCP
and SFTP, absolute and relative client paths, file create and mkdir,
plus two sanity checks for legitimate nested operations.
* fix: remove dead starts_with check after ParentDir clamping in chroot resolvers
In both ScpHandler and SftpHandler, the relative-path chroot resolver
initializes `resolved` to `root` and only extends it via Normal pushes.
The `ParentDir` arm already guards against popping past `root` with
`if resolved != root`, making the subsequent `if !resolved.starts_with(root)`
check unreachable dead code. Remove it and document why the single guard
is sufficient.
Also update docs/man/bssh-server.8: document sftp.root and scp.root in
the configuration sections, add the intermediate-directory-symlink
chroot protection to SECURITY CONSIDERATIONS, and bump the version to
v2.1.3 to reflect the PR's unreleased changes.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,9 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
10
10
### Added
11
11
- Internal fork of `russh-sftp` as `crates/bssh-russh-sftp` with a `serde_bytes` performance fix for `SSH_FXP_WRITE` and `SSH_FXP_DATA` packets. The upstream serde derive routes `Vec<u8>` through `deserialize_seq` (byte-by-byte), accounting for ~42% of server CPU during 1 GiB SFTP uploads in `perf` profiling. Annotating the `data` fields with `#[serde(with = "serde_bytes")]` and implementing wire-compatible `serialize_bytes` on the SFTP `Serializer` routes through the existing bulk `deserialize_byte_buf`/`try_get_bytes` path. Measured impact on a CPU-bound host (Xeon Silver 4214): 1 GiB SFTP upload throughput improves from 74.8 MiB/s to 96.4 MiB/s (+29%), closing the gap to OpenSSH `sftp-server` from ~26% to ~5%.
12
+
-`scp.root` configuration field. SCP transfers now honor a chroot setting separate from SFTP. When unset, SCP falls back to `sftp.root`, so a single top-level chroot setting governs both subsystems unless an admin explicitly wants them split.
12
13
13
14
### Changed
14
15
- Switched the top-level `russh-sftp` dependency from crates.io `russh-sftp = "2.1.1"` to `russh-sftp = { package = "bssh-russh-sftp", version = "2.1.1", path = "crates/bssh-russh-sftp" }`. All existing `use russh_sftp::...` imports continue to work unchanged.
16
+
-**Default file-transfer behavior is no longer chrooted to the user's home directory.** With `sftp.root`/`scp.root` unset (the default), absolute client paths are honored verbatim and relative paths resolve from the user's home directory, matching OpenSSH `sftp-server`/`scp` defaults. Deployments that intentionally want chroot-at-home-dir must now set `sftp.root: <home dir>` (or equivalent) explicitly. (#186)
17
+
18
+
### Fixed
19
+
-**bssh-server SCP/SFTP path doubling on absolute client paths** (#186). `ScpHandler::resolve_path` and `SftpHandler::resolve_path_static` previously re-rooted every absolute client path under the user's home directory, so `scp local user@host:/home/work/file.bin` wrote to `/home/work/home/work/file.bin` and `bssh upload local /abs/remote.bin` failed with `No such file`. The resolver now treats absolute client paths verbatim when no chroot is configured and rejects out-of-chroot absolute paths with `permission_denied` when one is. Path-traversal and symlink-escape protections continue to apply.
20
+
-**SCP single-file destinations no longer have the source filename appended** (#186). `ScpHandler::receive_file` now consults `target_is_directory` (parsed from `-d`/`-r`) and the filesystem state of the resolved target. `scp local.bin user@host:/tmp/dest.bin` now writes to `/tmp/dest.bin` instead of `/tmp/dest.bin/local.bin`. Directory destinations (`/tmp/dir/`, existing directory, or `-d`/`-r` flag) keep the previous filename-appending behavior.
21
+
-**Configured `sftp.root` is no longer dead code** (#186). The handler-construction sites in `SshHandler` previously hard-coded `user_info.home_dir` as the chroot root and ignored `config.sftp.root` entirely. Setting `sftp.root` in the YAML configuration now actually changes the SFTP chroot. The same plumbing now exists for `scp.root`.
22
+
-**Chroot bypass via intermediate-directory symlink**. The chroot resolver previously checked only lexical containment for paths whose final component did not exist (typical for new-file creates and `mkdir`). A symlink inside the chroot pointing to a directory outside the chroot would let a client target `chroot/escape/newfile` and have `open(...)`/`create_dir(...)` follow the symlink, writing outside the chroot. Both `ScpHandler::resolve_path` and `SftpHandler::resolve_path_static` now canonicalize the closest existing ancestor of the target path and verify it stays inside the canonicalized chroot, blocking the parent-symlink escape. Found during PR #194 review.
0 commit comments