{"record":{"id":"b978b60e14a90fa7","repo":"NousResearch/hermes-agent","slug":"unsafe-ssh-target-host-contains-control-character","errorCode":null,"errorMessage":"Unsafe SSH target: host contains control characters.","messagePattern":"Unsafe SSH target: host contains control characters\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/ssh-connection.ts","lineNumber":57,"sourceCode":"const DEFAULT_CONNECT_TIMEOUT_MS = 15_000\nconst DEFAULT_EXEC_TIMEOUT_MS = 20_000\nconst DEFAULT_FORWARD_TIMEOUT_MS = 15_000\nconst CONTROL_PERSIST_SECONDS = 300\n\n// eslint-disable-next-line no-control-regex -- deliberately reject control chars in ssh targets\nconst _CONTROL_CHAR_RE = /[\\x00-\\x1f\\x7f]/\n\nfunction validateSshTarget(host, user, port) {\n  if (!host || typeof host !== 'string') {\n    throw new Error('Unsafe SSH target: host is required.')\n  }\n\n  if (host.startsWith('-')) {\n    throw new Error(`Unsafe SSH target: host must not start with a dash (\"${host}\").`)\n  }\n\n  if (_CONTROL_CHAR_RE.test(host)) {\n    throw new Error('Unsafe SSH target: host contains control characters.')\n  }\n\n  if (user && _CONTROL_CHAR_RE.test(user)) {\n    throw new Error('Unsafe SSH target: user contains control characters.')\n  }\n\n  if (user && user.startsWith('-')) {\n    throw new Error(`Unsafe SSH target: user must not start with a dash (\"${user}\").`)\n  }\n\n  const p = Number(port)\n\n  if (!Number.isInteger(p) || p < 1 || p > 65535) {\n    throw new Error(`Unsafe SSH port: ${port} (must be 1-65535).`)\n  }\n}\n\nfunction validateKeyPath(keyPath) {","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/ssh-connection.ts#L39-L75","documentation":"Thrown by validateSshTarget() when the host matches _CONTROL_CHAR_RE (/[\\x00-\\x1f\\x7f]/). Control characters in a host can corrupt terminal output, break command construction, or smuggle escapes, so they are rejected before the ssh command line is assembled.","triggerScenarios":"Host strings containing tab, newline, bell, escape (\\x1b), NUL, or DEL — from pasted text with invisible characters, corrupted config, or crafted input.","commonSituations":"Copy-pasting a hostname from a PDF/chat that includes an invisible control byte; config with embedded escape sequences; a terminal paste containing a trailing \\r (CRLF) that survives into the stored host.","solutions":["Re-type the hostname manually, or sanitize: host.replace(/[\\x00-\\x1f\\x7f]/g, '').trim().","Hex-inspect the stored config entry to find which control byte is present.","Validate at input time in the UI with the same character class."],"exampleFix":"// before\nconst conn = new SshConnection({ host: rawHost, ... })\n\n// after\nconst host = rawHost.replace(/[\\x00-\\x1f\\x7f]/g, '').trim()\nif (!host) throw new TypeError('host empty after sanitizing control characters')\nconst conn = new SshConnection({ host, ... })","handlingStrategy":"validation","validationCode":"const CONTROL = /[\\x00-\\x1f\\x7f]/\nif (typeof host !== 'string' || CONTROL.test(host)) {\n  rejectConfig('SSH host contains control characters')\n}","typeGuard":"function isControlFreeString(v: unknown): v is string {\n  return typeof v === 'string' && !/[\\x00-\\x1f\\x7f]/.test(v)\n}","tryCatchPattern":"try {\n  validateSshTarget(host, user, port)\n} catch (e) {\n  if (e instanceof Error && e.message === 'Unsafe SSH target: host contains control characters.') {\n    host = host.replace(/[\\x00-\\x1f\\x7f]/g, '').trim()\n    if (!host) throw e\n    validateSshTarget(host, user, port) // re-validate sanitized value\n  } else throw e\n}","preventionTips":["Sanitize pasted hostnames with the same character class before storing.","Hex-inspect stored config when connections fail with no visible cause.","Validate host, user, and keyPath with one shared control-char check at config save time."],"tags":["ssh","security","validation","sanitization"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}