{"record":{"id":"3826560d35d46e2c","repo":"Panniantong/Agent-Reach","slug":"ssrf-blocked-only-public-http-s-urls-are-allowed","errorCode":null,"errorMessage":"SSRF blocked: only public http(s) URLs are allowed","messagePattern":"SSRF blocked: only public http\\(s\\) URLs are allowed","errorType":"exception","errorClass":"TranscribeError","httpStatus":null,"severity":"error","filePath":"agent_reach/transcribe.py","lineNumber":221,"sourceCode":"        (\n            ip.is_private,\n            ip.is_loopback,\n            ip.is_link_local,\n            ip.is_reserved,\n            ip.is_multicast,\n            ip.is_unspecified,\n        )\n    )\n\n\ndef _assert_safe_public_url(url: str) -> None:\n    \"\"\"Reject literal local/internal URLs without DNS-resolving public hosts.\"\"\"\n    if \"://\" not in url:\n        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\")","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/Panniantong/Agent-Reach/blob/93ae1d18c37b707dec053c7c4f9d91cd8ef8943d/agent_reach/transcribe.py#L203-L239","documentation":"Raised by _assert_safe_public_url (transcribe.py:216-221) during SSRF validation when the URL has no '://' scheme and the text before the first slash contains a colon whose part after the last colon is not pure digits — i.e. it looks like a scheme or malformed port rather than host:port. Scheme-less input is auto-prefixed with https:// only when it plausibly is a bare host[:port].","triggerScenarios":"download_audio('ftp://...') goes to the scheme branch (error 64); this specific raise fires for inputs like 'example.com:notaport', 'rtsp:media', 'mailto:x', or a bare 'https:' fragment without '//'. Any scheme-less string whose colon suffix is non-numeric is rejected.","commonSituations":"Passing a URI copied with a custom scheme (rtsp:, rtmp:) or a mangled copy-paste like 'example.com:8080:extra'. Developers testing download_audio() with non-HTTP media URLs (RTSP cameras, FTP archives).","solutions":["Pass a full http(s) URL including scheme: 'https://example.com/file.mp3'","For bare hosts, use host or host:digits form ('example.com:8443') which normalizes correctly","For rtsp/ftp/other-protocol sources, download the media yourself first, then pass the local file path to transcribe()"],"exampleFix":"# before\ndownload_audio(\"rtsp:camera-stream\", out_dir)  # SSRF blocked\n\n# after: fetch out-of-band, hand over a local file\nsubprocess.run([\"ffmpeg\", \"-rtsp_transport\", \"tcp\", \"-i\", \"rtsp://cam/stream\", \"-t\", \"60\", \"cam.m4a\"], check=True)\ntranscribe(\"cam.m4a\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef is_http_url(url: str) -> bool:\n    if \"://\" in url:\n        return urlparse(url).scheme in {\"http\", \"https\"}\n    before_slash = url.split(\"/\", 1)[0]\n    if \":\" in before_slash:\n        _, port = before_slash.rsplit(\":\", 1)\n        return port.isdigit()\n    return True","typeGuard":null,"tryCatchPattern":"from agent_reach.transcribe import TranscribeError\ntry:\n    transcribe(source)\nexcept TranscribeError as e:\n    if str(e).startswith(\"SSRF blocked\"):\n        reject_user_input(source)  # do not retry; fix the URL\n    raise","preventionTips":["Always pass full https:// URLs for remote media","Convert rtsp/ftp/other-protocol sources to local files before transcribing","Validate scheme-less strings for a proper host[:digits] shape"],"tags":["ssrf","url-validation","security","transcription"],"backgroundTag":null,"analyzedSha":"93ae1d18c37b707dec053c7c4f9d91cd8ef8943d","analyzedAt":"2026-08-14T22:54:06.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}