{"record":{"id":"42f1624aa41300d8","repo":"can1357/oh-my-pi","slug":"invalid-ssh-username-username-an-ssh-usernam","errorCode":null,"errorMessage":"Invalid SSH username \"${username}\": an SSH username must not begin with \"-\" (argument-injection guard)","messagePattern":"Invalid SSH username \"(.+?)\": an SSH username must not begin with \"-\" \\(argument-injection guard\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/ssh/utils.ts","lineNumber":17,"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 \"''\";\n\treturn `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n\n/**\n * Wrap a POSIX command in `<shell> -c '<command>'` so it runs under the","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/ssh/utils.ts#L1-L35","documentation":"Companion to the host guard in buildSshTarget: SSH also parses `user@host` destinations where a username starting with \"-\" would let an option like `-oProxyCommand=...` slip through before the @ separator in some parse paths. The library rejects any username beginning with \"-\" before the target string reaches an ssh argv.","triggerScenarios":"Calling buildSshTarget with a username string starting with \"-\", e.g. from a URL like ssh://-oProxyCommand=evil@host or a config where the user field contains ssh options.","commonSituations":"Malformed ssh:// URLs where the userinfo component holds options, attacker-crafted remote URLs attempting option injection, or copy-paste errors putting flags into the username field.","solutions":["Correct the username in the ssh:// URL or config so it contains no leading dash.","Pass ssh options via ~/.ssh/config or the tool's option fields instead of the username.","Validate the username before calling buildSshTarget."],"exampleFix":"// before\nbuildSshTarget(\"-oProxyCommand=evil\", \"host.example.com\")\n// after\nbuildSshTarget(\"deploy\", \"host.example.com\")","handlingStrategy":"validation","validationCode":"function safeSshUser(user: string): boolean { return !user.startsWith(\"-\"); }\nif (username && !safeSshUser(username)) throw new Error(\"username must not begin with '-'\");","typeGuard":null,"tryCatchPattern":"try { const target = buildSshTarget(username, host); } catch (err) { log.warn(\"ssh username rejected\", { username, err }); return; }","preventionTips":["Validate both components of ssh:// URLs (userinfo and host) before constructing destinations.","Reject any credential/identity string beginning with '-' at input boundaries.","Prefer explicit config fields for ssh options over embedding them in user@host strings."],"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"}