{"record":{"id":"a23bf85a9596cad6","repo":"odysseus-dev/odysseus","slug":"invalid-remote-host-must-be-host-or-user-host-n","errorCode":null,"errorMessage":"Invalid remote_host — must be host or user@host, no SSH option syntax","messagePattern":"Invalid remote_host — must be host or user@host, no SSH option syntax","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/_validators.py","lineNumber":16,"sourceCode":"import re\n\nfrom fastapi import HTTPException\n\n\n_REMOTE_HOST_RE = re.compile(\n    r\"^(?:[A-Za-z0-9][A-Za-z0-9._-]*@)?[A-Za-z0-9][A-Za-z0-9._-]*$\"\n)\n_SSH_PORT_RE = re.compile(r\"^\\d{1,5}$\")\n\n\ndef validate_remote_host(v: str | None) -> str | None:\n    if v is None or v == \"\":\n        return None\n    if not _REMOTE_HOST_RE.match(v):\n        raise HTTPException(\n            400,\n            \"Invalid remote_host — must be host or user@host, no SSH option syntax\",\n        )\n    return v\n\n\ndef validate_ssh_port(v: str | None) -> str | None:\n    if v is None or v == \"\":\n        return None\n    if not _SSH_PORT_RE.fullmatch(str(v)):\n        raise HTTPException(400, \"Invalid ssh_port\")\n    port = int(v)\n    if port < 1 or port > 65535:\n        raise HTTPException(400, \"Invalid ssh_port\")\n    return str(port)\n","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/_validators.py#L1-L32","documentation":"A FastAPI HTTPException(400) from validate_remote_host: the remote_host value failed the strict regex that permits only 'host' or 'user@host' forms (letters, digits, dots, underscores, hyphens). Anything carrying SSH option syntax — '-p 22', '-i key', 'ssh://...' — is rejected to prevent option injection into downstream ssh commands.","triggerScenarios":"POSTing a remote-host form/API payload with remote_host like 'ssh -p 2222 host', 'user@host -i ~/.ssh/id_ed25519', 'ssh://user@host:22', or an IPv6 literal with brackets/colons.","commonSituations":"Users pasting a full ssh command line into a host field; URLs instead of bare hostnames; bracketed IPv6 addresses which the character class does not allow.","solutions":["Send only 'user@host' or 'host'; move the port into the separate ssh_port field","Remove ssh:// scheme prefixes and option flags","For IPv6, file a feature request / use a hostname, since the regex rejects colons and brackets"],"exampleFix":"# before\nremote_host=\"user@host -p 2222\"\n# after\nremote_host=\"user@host\", ssh_port=2222","handlingStrategy":"validation","validationCode":"import re\nREMOTE = re.compile(r'^(?:[A-Za-z0-9][A-Za-z0-9._-]*@)?[A-Za-z0-9][A-Za-z0-9._-]*$')\nif not REMOTE.match(remote_host or ''):\n    remote_host = remote_host.split()[-1].lstrip('ssh://')  # or reject client-side","typeGuard":"def is_valid_remote_host(v: str | None) -> bool:\n    return v is None or bool(re.match(r'^(?:[A-Za-z0-9][A-Za-z0-9._-]*@)?[A-Za-z0-9][A-Za-z0-9._-]*$', v))","tryCatchPattern":"try:\n    resp = client.post('/api/remote', data={'remote_host': h})\nexcept HTTPError as e:\n    if e.response.status_code == 400 and 'remote_host' in e.response.text:\n        h = extract_bare_host(h); retry()\n    raise","preventionTips":["Split host and port into separate fields in the UI","Strip ssh:// and flags before submitting","Add client-side regex mirroring the server rule"],"tags":["http","validation","ssh","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}