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
cli(cluster): #2861 leaves ssh argument-injection open — a machine.host/user starting with - runs an arbitrary command on the local machine (ProxyCommand) #3133
This issue was created by a scheduled, automated Claude review of recent commits/PRs. It was verified by reading the code directly, but please double-check before acting on it.
Summary
PR #2861 (commit 9b288f5, "fix(cli): validate cluster machine id and labels before they reach the remote shell") hardens dora cluster against remote-shell injection by validating machine.id, label keys/values, and zenoh_peer. It deliberately leaves machine.host and machine.user unvalidated, with this stated rationale (binaries/cli/src/command/cluster/config.rs:92-96):
host/user are deliberately left unrestricted: they are network addresses that legitimately carry characters outside this set (e.g. IPv6 literals), and are folded into the ssh target rather than the remote command string.
That reasoning only rules out injection into the remote shell. It overlooks injection into the local ssh client's own argument parser, which is a separate and arguably more severe vector: it executes on the operator's machine, before any network connection.
and run_ssh passes that target to ssh with no -- end-of-options separator (binaries/cli/src/command/cluster/mod.rs:92-105):
pub(super)fnrun_ssh(target:&str,port:Option<u16>,cmd:&str) -> eyre::Result<bool>{letmut command = std::process::Command::new("ssh");
command.args(["-o","BatchMode=yes",/* … */]);ifletSome(p) = port { command.args(["-p",&p.to_string()]);}
command.args([target, cmd]);// <-- target is unguarded; no "--" before it// …}
If host (or user) begins with -, the local ssh binary parses target as an option, not a hostname. This is the classic ssh/git argument-injection class (cf. CVE-2017-1000117).
Concrete exploit
A cluster descriptor is the untrusted input the rest of #2861 is defending against. Given:
ssh interprets -oProxyCommand=… and runs touch /tmp/pwned;falseon the machine running dora cluster up (via the local shell that ProxyCommand uses) before/instead of connecting. Arbitrary local command execution.
user is an equivalent vector: ssh_target yields {user}@{host}, so a user beginning with - produces a target string that also starts with -.
Affected code paths
run_ssh / ssh_target are the shared SSH primitives for the whole dora cluster surface, so every subcommand that shells out is affected:
binaries/cli/src/command/cluster/up.rs (ssh_target at ~:66, ~:78)
install.rs, uninstall.rs, upgrade.rs (all route through run_ssh)
The host/user values originate from MachineConfig parsed out of the cluster YAML and are never checked for a leading -.
Suggested fix
Two complementary defenses (the codebase already treats the cluster descriptor as an injection boundary, so validation is consistent with its own posture):
Reject host/user values that begin with - in ClusterConfig::validate() (git's portable mitigation for exactly this class). This keeps IPv6 literals and normal user@host values valid while closing the option-injection hole. It can't be folded into validate_shell_safe (which would reject the :/[/] in IPv6), so it needs a dedicated leading-dash check.
Insert a literal "--" before target in run_ssh — command.args(["--", target, cmd]). OpenSSH accepts -- as an end-of-options marker; this is defense-in-depth for the local argv, and because all subcommands share run_ssh, one change covers them all. (Note -- support is version-dependent on very old OpenSSH, so prefer Set up a basic Rust project #1 as the primary fix and treat #2 as belt-and-suspenders.)
Notes / non-issues found in the same review
A secondary, low-severity observation on the same PR: validate_endpoint_shell_safe permits [/] in zenoh_peer, which is interpolated unquoted into the remote command. Under default sh/bash (no nullglob/failglob) an unmatched [::1] glob is left literal, so it normally works; it is not command injection (no way to start a new command). Single-quoting the remote interpolations, or requiring the un-bracketed IPv6 form, would remove the glob ambiguity. Low priority.
Summary
PR #2861 (commit
9b288f5, "fix(cli): validate cluster machine id and labels before they reach the remote shell") hardensdora clusteragainst remote-shell injection by validatingmachine.id, label keys/values, andzenoh_peer. It deliberately leavesmachine.hostandmachine.userunvalidated, with this stated rationale (binaries/cli/src/command/cluster/config.rs:92-96):That reasoning only rules out injection into the remote shell. It overlooks injection into the local
sshclient's own argument parser, which is a separate and arguably more severe vector: it executes on the operator's machine, before any network connection.The vulnerability
ssh_targetinterpolateshost/userverbatim (binaries/cli/src/command/cluster/mod.rs:58-63):and
run_sshpasses that target tosshwith no--end-of-options separator (binaries/cli/src/command/cluster/mod.rs:92-105):If
host(oruser) begins with-, the localsshbinary parsestargetas an option, not a hostname. This is the classic ssh/git argument-injection class (cf. CVE-2017-1000117).Concrete exploit
A cluster descriptor is the untrusted input the rest of #2861 is defending against. Given:
the argv becomes:
sshinterprets-oProxyCommand=…and runstouch /tmp/pwned;falseon the machine runningdora cluster up(via the local shell thatProxyCommanduses) before/instead of connecting. Arbitrary local command execution.useris an equivalent vector:ssh_targetyields{user}@{host}, so auserbeginning with-produces a target string that also starts with-.Affected code paths
run_ssh/ssh_targetare the shared SSH primitives for the wholedora clustersurface, so every subcommand that shells out is affected:binaries/cli/src/command/cluster/up.rs(ssh_targetat ~:66, ~:78)install.rs,uninstall.rs,upgrade.rs(all route throughrun_ssh)The
host/uservalues originate fromMachineConfigparsed out of the cluster YAML and are never checked for a leading-.Suggested fix
Two complementary defenses (the codebase already treats the cluster descriptor as an injection boundary, so validation is consistent with its own posture):
host/uservalues that begin with-inClusterConfig::validate()(git's portable mitigation for exactly this class). This keeps IPv6 literals and normaluser@hostvalues valid while closing the option-injection hole. It can't be folded intovalidate_shell_safe(which would reject the:/[/]in IPv6), so it needs a dedicated leading-dash check."--"beforetargetinrun_ssh—command.args(["--", target, cmd]). OpenSSH accepts--as an end-of-options marker; this is defense-in-depth for the local argv, and because all subcommands sharerun_ssh, one change covers them all. (Note--support is version-dependent on very old OpenSSH, so prefer Set up a basic Rust project #1 as the primary fix and treat #2 as belt-and-suspenders.)Notes / non-issues found in the same review
validate_endpoint_shell_safepermits[/]inzenoh_peer, which is interpolated unquoted into the remote command. Under defaultsh/bash(nonullglob/failglob) an unmatched[::1]glob is left literal, so it normally works; it is not command injection (no way to start a new command). Single-quoting the remote interpolations, or requiring the un-bracketed IPv6 form, would remove the glob ambiguity. Low priority.