{"record":{"id":"3172382078f55627","repo":"Panniantong/Agent-Reach","slug":"ssrf-blocked-encoded-or-ambiguous-url-host","errorCode":null,"errorMessage":"SSRF blocked: encoded or ambiguous URL host","messagePattern":"SSRF blocked: encoded or ambiguous URL host","errorType":"exception","errorClass":"TranscribeError","httpStatus":null,"severity":"error","filePath":"agent_reach/transcribe.py","lineNumber":235,"sourceCode":"        before_slash = url.split(\"/\", 1)[0]\n        if \":\" in before_slash:\n            host_part, port_part = before_slash.rsplit(\":\", 1)\n            if not host_part or not port_part.isdigit():\n                raise TranscribeError(\"SSRF blocked: only public http(s) URLs are allowed\")\n        normalized_url = f\"https://{url}\"\n        parsed = urlparse(normalized_url)\n    else:\n        normalized_url = url\n        parsed = urlparse(url)\n        if parsed.scheme not in {\"http\", \"https\"}:\n            raise TranscribeError(\"SSRF blocked: only public http(s) URLs are allowed\")\n\n    raw_authority = normalized_url.split(\"://\", 1)[1]\n    raw_authority = raw_authority.split(\"/\", 1)[0]\n    raw_authority = raw_authority.split(\"?\", 1)[0]\n    raw_authority = raw_authority.split(\"#\", 1)[0]\n    if \"\\\\\" in raw_authority or \"%\" in raw_authority:\n        raise TranscribeError(\"SSRF blocked: encoded or ambiguous URL host\")\n\n    raw_host = (parsed.hostname or \"\").strip().rstrip(\".\")\n    if not raw_host:\n        raise TranscribeError(\"SSRF blocked: URL host is missing\")\n    try:\n        host = raw_host.encode(\"idna\").decode(\"ascii\").lower().rstrip(\".\")\n    except UnicodeError:\n        raise TranscribeError(\"SSRF blocked: URL host is invalid\") from None\n    if host in _BLOCKED_HOSTS or host.endswith(\".localhost\"):\n        raise TranscribeError(\"SSRF blocked: internal host is not allowed\")\n    if _is_private_ip(host):\n        raise TranscribeError(\"SSRF blocked: private/internal IP is not allowed\")\n\n\ndef download_audio(url: str, out_dir: Path) -> Path:\n    \"\"\"Download audio with yt-dlp into out_dir; return the resulting file path.\"\"\"\n    _assert_safe_public_url(url)\n    _require(\"yt-dlp\")","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/Panniantong/Agent-Reach/blob/93ae1d18c37b707dec053c7c4f9d91cd8ef8943d/agent_reach/transcribe.py#L217-L253","documentation":"Raised by _assert_safe_public_url (transcribe.py:230-235) when the URL's authority segment (between '://' and the first /, ?, or #) contains a backslash or percent sign. These characters create parser ambiguity — different consumers (urlparse vs yt-dlp's extractor vs the C resolver) can disagree about where the host ends, a classic SSRF-bypass technique, so the guard fails closed.","triggerScenarios":"URLs like 'https://example.com%2f@evil.com/a', 'https://evil.com\\@example.com/a', or percent-encoded hosts ('https://%65xample.com/'). Also triggered by Windows-style paths mistakenly passed as URLs ('https://C:\\\\audio\\\\file.m4a').","commonSituations":"Copy-pasting URLs that carry encoded credentials or path traversal fragments; feeding Windows paths; deliberate SSRF probe payloads in security scanning of agent-facing endpoints.","solutions":["Use a plain, canonical host with no percent-encoding or backslashes: 'https://example.com/media.mp3'","If the URL came from user/LLM input, normalize with urllib.parse and re-encode components properly before passing","Remove inline credentials ('user:pass@') — pass a clean host[:port]/path instead"],"exampleFix":"# before\nurl = \"https://example.com%2f@evil.com/audio.mp3\"  # SSRF blocked: encoded or ambiguous URL host\n\n# after\nfrom urllib.parse import urlsplit, urlunsplit\nparts = urlsplit(url)\nurl = urlunsplit((parts.scheme, parts.netloc.rsplit(\"@\", 1)[-1], parts.path, parts.query, \"\"))\n# -> https://evil.com/audio.mp3 (validate that host is truly intended)","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef has_clean_authority(url: str) -> bool:\n    try:\n        p = urlparse(url)\n    except ValueError:\n        return False\n    if p.scheme not in {\"http\", \"https\"} or not p.hostname:\n        return False\n    auth = (p.netloc.rsplit(\"@\", 1)[-1]\n            .split(\"/\", 1)[0].split(\"?\", 1)[0].split(\"#\", 1)[0])\n    return \"\\\\\" not in auth and \"%\" not in auth","typeGuard":null,"tryCatchPattern":"from agent_reach.transcribe import TranscribeError\ntry:\n    transcribe(url)\nexcept TranscribeError as e:\n    if \"encoded or ambiguous URL host\" in str(e):\n        url = canonicalize(url)  # strip credentials, re-encode properly\n        return transcribe(url)\n    raise","preventionTips":["Strip inline credentials and percent-encoded hosts from URLs before use","Rebuild URLs from urlsplit parts instead of string surgery","Never pass Windows-style backslash paths as URLs"],"tags":["ssrf","url-validation","security","encoding"],"backgroundTag":null,"analyzedSha":"93ae1d18c37b707dec053c7c4f9d91cd8ef8943d","analyzedAt":"2026-08-14T22:54:06.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}