{"record":{"id":"f3ada7c5b334b8c5","repo":"can1357/oh-my-pi","slug":"invalid-ssh-host-host-an-ssh-destination-mus","errorCode":null,"errorMessage":"Invalid SSH host \"${host}\": an SSH destination must not begin with \"-\" (argument-injection guard)","messagePattern":"Invalid SSH host \"(.+?)\": an SSH destination must not begin with \"-\" \\(argument-injection guard\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/ssh/utils.ts","lineNumber":12,"sourceCode":"export function sanitizeHostName(name: string): string {\n\tconst sanitized = name.replace(/[^a-zA-Z0-9._-]+/g, \"_\");\n\treturn sanitized.length > 0 ? sanitized : \"remote\";\n}\n\nexport function buildSshTarget(username: string | undefined, host: string): string {\n\t// SSH treats a destination starting with \"-\" as an option, so a host/user of\n\t// `-oProxyCommand=...` becomes local command execution. Reject before this\n\t// string reaches any `ssh` argv (this is the single render chokepoint for\n\t// every connection, transfer, and sshfs mount).\n\tif (host.startsWith(\"-\")) {\n\t\tthrow new Error(\n\t\t\t`Invalid SSH host \"${host}\": an SSH destination must not begin with \"-\" (argument-injection guard)`,\n\t\t);\n\t}\n\tif (username?.startsWith(\"-\")) {\n\t\tthrow new Error(\n\t\t\t`Invalid SSH username \"${username}\": an SSH username must not begin with \"-\" (argument-injection guard)`,\n\t\t);\n\t}\n\treturn username ? `${username}@${host}` : host;\n}\n\n/**\n * Single-quote a path for a POSIX remote shell, escaping embedded single quotes.\n * Mirrors the private `quoteRemotePath` in `tools/ssh.ts`; shared here for the\n * `ssh://` file-transfer helpers.\n */\nexport function quotePosixPath(value: string): string {\n\tif (value.length === 0) return \"''\";","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/ssh/utils.ts#L1-L30","documentation":"buildSshTarget is the single chokepoint that renders every SSH destination string used by connections, transfers, and sshfs mounts. SSH parses a destination argument starting with \"-\" as an option flag, so a hostile or corrupted host value like `-oProxyCommand=...` would be executed locally as an ssh option instead of treated as a hostname. The library throws this error to guarantee no user-controlled string can be injected into the ssh argv as an option.","triggerScenarios":"Calling buildSshTarget (directly or via buildRemoteCommand/target) with a host string whose first character is \"-\", e.g. from a malformed ssh:// URL, a config value like `-oProxyCommand=evil`, or a parsed destination where the option separator was lost.","commonSituations":"A mistyped ssh:// URL such as ssh://-oProxyCommand=x@host, a config file with the host field holding ssh options instead of a hostname, or an attacker-controlled remote/repository URL attempting argument injection against tooling that shells out to ssh.","solutions":["Fix the host value in your ssh:// URL or configuration so it is a real hostname (no leading dash).","If you need ssh options (ProxyJump, port, etc.), pass them through the dedicated option fields or ~/.ssh/config, not inside the host string.","Validate/sanitize the host before it reaches buildSshTarget, e.g. strip a scheme and reject leading dashes."],"exampleFix":"// before\nbuildSshTarget(undefined, \"-oProxyCommand=evil\")\n// after\nbuildSshTarget(undefined, \"jump.example.com\") // put ProxyCommand in ~/.ssh/config","handlingStrategy":"validation","validationCode":"function safeSshHost(host: string): boolean { return host.length > 0 && !host.startsWith(\"-\"); }\nif (!safeSshHost(host)) throw new Error(\"host must not begin with '-'\");","typeGuard":null,"tryCatchPattern":"try { const target = buildSshTarget(user, host); } catch (err) { log.warn(\"ssh target rejected\", { host, err }); return; }","preventionTips":["Never build ssh destinations from raw URL components or user config without checking for a leading dash.","Keep ssh options in ~/.ssh/config or dedicated option fields, never inside host/username strings.","Treat any remote URL (repo remotes, sync targets) as untrusted input before passing to ssh."],"tags":["security","argument-injection","ssh"],"backgroundTag":"ssh-argument-injection","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}