fix(cli,tui): escape and validate SSH session response fields#876
Open
johntmyers wants to merge 1 commit intomainfrom
Open
fix(cli,tui): escape and validate SSH session response fields#876johntmyers wants to merge 1 commit intomainfrom
johntmyers wants to merge 1 commit intomainfrom
Conversation
The CLI and TUI build an SSH ProxyCommand string by interpolating fields from CreateSshSessionResponse into a shell command. Three fields - sandbox_id, token, and the assembled gateway_url (composed from gateway_scheme, gateway_host, gateway_port, connect_path) - were not passed through shell_escape, even though exe_command and gateway_name were. The resulting string is handed to `ssh -o ProxyCommand=...` which OpenSSH routes through `/bin/sh -c`, so a hostile or compromised gateway could inject shell metacharacters and execute arbitrary commands under the developer's workstation account. - Add build_proxy_command in openshell-core::forward that wraps every interpolated value in shell_escape. Use it at all four ProxyCommand sites: openshell-cli/src/ssh.rs (ssh_session_config) and the three TUI sites in openshell-tui/src/lib.rs (shell connect, sandbox exec, port-forward reconciliation). - Add validate_ssh_session_response in openshell-core::forward, called at each site immediately after response.into_inner(). Enforces a conservative charset per field (sandbox_id, token, gateway_host, gateway_scheme, gateway_port, connect_path, host_key_fingerprint) so malformed responses fail loudly at the gRPC trust boundary before shell escaping is attempted. Belt-and-suspenders with the escape. - Harden render_ssh_config so `gateway` and `name` are shell-escaped even though validate_gateway_name currently gates `gateway`. - Document the charset contract on CreateSshSessionResponse in proto/openshell.proto: servers must uphold these rules and clients reject responses that violate them. - Add regression unit tests covering adversarial values in every field and a build_proxy_command test asserting no shell metacharacter remains outside single-quoted regions. Tracks OS-100.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ProxyCommandstring by interpolating fields fromCreateSshSessionResponse(sandbox_id,token, and the assembled gateway URL) into a shell command. These three fields were not passed throughshell_escape, even thoughexe_commandandgateway_namein the same format strings were.ProxyCommandthrough/bin/sh -c, so a hostile or compromised gateway could inject shell metacharacters ($(...), backticks,;,|) and execute arbitrary commands on the developer's workstation under their user privileges.build_proxy_commandhelper inopenshell-core::forwardthat shell-escapes every interpolated argument, and adds avalidate_ssh_session_responseboundary validator that rejects malformed responses at the gRPC trust boundary — belt-and-suspenders with the escape.Related Issue
OS-100
Changes
crates/openshell-core/src/forward.rsbuild_proxy_command(exe, gateway_url, sandbox_id, token, gateway_name)— one source of truth for the ProxyCommand format string; every argument goes throughshell_escape.validate_ssh_session_response(&CreateSshSessionResponse)+SshSessionResponseErrorenum. Enforces conservative charsets per field:sandbox_id[A-Za-z0-9._-]{1,128},tokenURL-safe ASCII up to 4096 bytes,gateway_hostIP/bracketed-IPv6/DNS hostname ≤253 bytes,gateway_schemeexactlyhttp|https,gateway_portin1..=65535,connect_pathRFC 3986 path charset with mandatory leading/, optionalhost_key_fingerprint[A-Za-z0-9:+/=-]only.crates/openshell-cli/src/ssh.rsssh_session_configcallsvalidate_ssh_session_responseimmediately afterresponse.into_inner(), then usesbuild_proxy_command.render_ssh_configshell-escapesgatewayandname(defense-in-depth —validate_gateway_namealready gatesgateway).crates/openshell-tui/src/lib.rsbuild_proxy_command; validation failures surface viaapp.status_text/tracing::warn!.proto/openshell.protoCreateSshSessionResponsefields. Servers must uphold; clients reject.Testing
cargo test -p openshell-core— 110 tests pass, including 13 new tests for the validator andbuild_proxy_command.cargo test -p openshell-cli --lib— 78 tests pass.cargo test -p openshell-tui --lib— 12 tests pass.mise run pre-commit— passes (lint, format, license headers, rust tests).build_proxy_command_escapes_shell_metacharactersbuilds the command with attacker-controlled values (x$(touch /tmp/pwn)x,tok`id`) and asserts no$, backtick,|,;,&, or newline appears outside single-quoted regions.validate_ssh_session_response_rejects_*coverssandbox_id="a;b",token="$(id)",gateway_host="evil; cmd",gateway_scheme="javascript",gateway_port=0/70000,connect_path="/x+ backtick +id+ backtick +y"/"no-leading-slash"/"/a b"/"/a\nb".Checklist