Skip to content

Prevent SSH option/shell injection via store URLs#351

Merged
folbricht merged 1 commit into
masterfrom
fix-ssh-option-injection
May 19, 2026
Merged

Prevent SSH option/shell injection via store URLs#351
folbricht merged 1 commit into
masterfrom
fix-ssh-option-injection

Conversation

@folbricht
Copy link
Copy Markdown
Owner

Problem

SFTPStore (sftp.go) and RemoteSSH (remotessh.go) build their ssh destination from the store URL (user@host) and pass it as the first positional argument to ssh. The destination is never validated or guarded with --.

  • Option injection — a store URL whose host begins with - (e.g. sftp://-oProxyCommand=touch${IFS}pwned/) is parsed by ssh as a command-line option rather than a destination, leading to arbitrary local command execution as the desync process user. Same class as Git CVE-2017-1000117 and Mercurial CVE-2017-1000116. Store URLs come from CLI flags and from --store-file JSON config that chunk-server/mount-index re-read on SIGHUP, so a less-privileged writer of that file can trigger it.
  • Remote shell injectionremotessh.go interpolated the URL path into the remote command string with only single-quote wrapping (fmt.Sprintf("%s pull - - - '%s'", remoteCmd, path)), so a ' in the path breaks out and injects into the sh -c the remote runs.

Verified on OpenSSH 9.9p1: ssh -oProxyCommand=id host parses as an option, while ssh -- -oProxyCommand=id host treats it as a destination.

Fix

Applied at the library boundary (newSFTPStoreBase, StartProtocol) so both the CLI and direct library users are protected; sftpindex.go shares newSFTPStoreBase and is covered automatically.

  • validateSSHHost rejects any destination beginning with -. This is the portable guarantee — it also protects when CASYNC_SSH_PATH points at an ssh implementation that doesn't honor --.
  • Pass -- before the destination as defense in depth (ssh -s -- <host> sftp, ssh -- <host> <cmd>). Confirmed the reordered sftp invocation still invokes the subsystem.
  • shellQuote POSIX-escapes the path before embedding it in the remote command string.

CASYNC_REMOTE_PATH is left interpolated unquoted on purpose: it is operator-controlled and quoting it would break legitimate casync <args> usage.

Tests

New sshcmd_test.go:

  • TestValidateSSHHost — rejects -, -x, -oProxyCommand=...; accepts host, user@host, host:port, mid-string dashes.
  • TestShellQuote — round-trips adversarial inputs (o'brien, a'; touch pwned; ', $VAR ${IFS} "dq" |&;<>, etc.) through a real sh -c and asserts byte-for-byte equality.

go test ./... and go build -o cmd/desync/ ./cmd/desync pass.

SFTPStore and RemoteSSH built their ssh destination from the store URL
(user@host) and passed it as the first positional argument to ssh. A URL
whose host begins with '-' (e.g. sftp://-oProxyCommand=.../) was parsed by
ssh as a command-line option instead of a destination, allowing arbitrary
local command execution as the desync process user -- the same class of
bug as Git CVE-2017-1000117 and Mercurial CVE-2017-1000116. Store URLs
come from CLI flags and from --store-file JSON config re-read on SIGHUP,
so a less-privileged writer of that file could trigger it.

RemoteSSH additionally interpolated the URL path into the remote command
string with only single-quote wrapping, so a single quote in the path
allowed remote-side shell injection.

Fixes:
- validateSSHHost rejects any destination beginning with '-' (portable
  guarantee, covers non-OpenSSH CASYNC_SSH_PATH implementations).
- Pass "--" before the destination so ssh can't parse it as an option
  (defense in depth). For sftp, "-s" now precedes "--".
- shellQuote escapes the path before embedding it in the remote command.

CASYNC_REMOTE_PATH is still interpolated unquoted; it is operator-
controlled and quoting it would break legitimate "casync <args>" usage.
@folbricht folbricht merged commit fcfe229 into master May 19, 2026
3 checks passed
@folbricht folbricht deleted the fix-ssh-option-injection branch May 19, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant