{"record":{"id":"23610d083e8b2f49","repo":"langchain-ai/langchain","slug":"exc","errorCode":null,"errorMessage":"{exc}","messagePattern":"\\{exc\\}","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"libs/core/langchain_core/_security/_ssrf_protection.py","lineNumber":82,"sourceCode":"    url_str = str(url)\n    parsed = urlparse(url_str)\n    hostname = parsed.hostname or \"\"\n\n    # Test-environment bypass (preserved from original implementation)\n    if (\n        os.environ.get(\"LANGCHAIN_ENV\") == \"local_test\"\n        and hostname.startswith(\"test\")\n        and \"server\" in hostname\n    ):\n        return url_str\n\n    policy = _policy_for(allow_private=allow_private, allow_http=allow_http)\n\n    # Synchronous scheme + hostname checks\n    try:\n        _validate_url_sync(url_str, policy)\n    except SSRFBlockedError as exc:\n        raise ValueError(str(exc)) from exc\n\n    # DNS resolution and IP validation\n    try:\n        addr_info = socket.getaddrinfo(\n            hostname,\n            parsed.port or (443 if parsed.scheme == \"https\" else 80),\n            socket.AF_UNSPEC,\n            socket.SOCK_STREAM,\n        )\n\n        for result in addr_info:\n            ip_str: str = result[4][0]  # type: ignore[assignment]\n            try:\n                _validate_resolved_ip(ip_str, policy)\n            except SSRFBlockedError as exc:\n                raise ValueError(str(exc)) from exc\n\n    except socket.gaierror as e:","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/langchain-ai/langchain/blob/e32fa9a52eab3b61ad7a45399bfde59b3e580fc4/libs/core/langchain_core/_security/_ssrf_protection.py#L64-L100","documentation":"This is the public `_ssrf_protection.py` helper (e.g. `check_url_ssrf` / url-for-fetch validation) re-raising an inner `SSRFBlockedError` from the synchronous scheme/hostname pre-check as a plain `ValueError(str(exc))`. The `{exc}` body is one of the SSRF reason strings — 'private IP range', 'localhost address', 'cloud metadata endpoint', 'Kubernetes internal DNS', 'scheme ... not allowed', or 'missing hostname' — so this error is the ValueError-facing envelope for all policy violations, chained via `from exc` to the original.","triggerScenarios":"Calling the public SSRF-check helper with a URL like `check_url('http://169.254.169.254/', ...)` or `check_url('http://localhost:8000/')` under default flags. The local-test escape hatch just above it returns early only when `LANGCHAIN_ENV == 'local_test'` AND the hostname starts with 'test' AND contains 'server' — any other host goes through full validation. Later in the same function DNS-resolved IPs are re-checked, so a hostname resolving to a blocked IP also lands here.","commonSituations":"Using a fetch utility built on this helper in local dev (localhost URLs), inside Kubernetes (`.svc` hostnames), or behind prompt-injected URLs in agent tools. Developers see a bare ValueError mentioning e.g. 'private IP range' and don't realize it comes from the SSRF layer or that `allow_private`/`allow_http` flags exist.","solutions":["Read the message text — it names the exact policy reason; apply the matching fix (allow_private=True for trusted private endpoints, allowed_hosts for specific hostnames, correct scheme/host in the URL).","For local dev, use `LANGCHAIN_ENV=local_test` with `test*server*` hostnames (the built-in bypass) or set `LANGCHAIN_ENV=local...` for the policy-level localhost allowlist.","Keep `allow_private`/`allow_http` relaxation limited to explicitly trusted, user-configured base URLs — never for model-chosen URLs."],"exampleFix":"# before\nchecked = check_url('http://192.168.0.5:9000/minio', allow_private=False)\n# ValueError: private IP range\n\n# after\nchecked = check_url('http://192.168.0.5:9000/minio', allow_private=True)","handlingStrategy":"try-catch","validationCode":"from urllib.parse import urlparse\nimport ipaddress, socket\n\ndef precheck_url(url: str) -> str | None:\n    \"\"\"Return the SSRF reason that would block this URL, or None.\"\"\"\n    p = urlparse(url)\n    if (p.scheme or \"\").lower() not in {\"http\", \"https\"}:\n        return f\"scheme '{p.scheme}' not allowed\"\n    host = (p.hostname or \"\").lower()\n    if not host:\n        return \"missing hostname\"\n    try:\n        if ipaddress.ip_address(host).is_private:\n            return \"private IP range\"\n    except ValueError:\n        pass\n    return None  # full policy check still required for DNS-resolved IPs","typeGuard":null,"tryCatchPattern":"try:\n    check_url(url, allow_private=host_is_trusted(url))\nexcept ValueError as e:\n    msg = str(e)\n    if any(s in msg for s in (\"private IP range\", \"localhost address\", \"cloud metadata\", \"Kubernetes\")):\n        raise BlockedBySSRFPolicy(url, msg) from e  # permanent: fix policy or URL\n    if \"DNS\" in msg:\n        retry_once()  # transient resolution failure\n    raise","preventionTips":["Pass allow_private/allow_http explicitly from trusted configuration, never from untrusted input.","For local dev, use LANGCHAIN_ENV=local_test with test*server* hostnames or LANGCHAIN_ENV=local for the localhost allowlist.","Map the ValueError message back to the underlying SSRF reason before deciding retry vs fix — only DNS failures are transient."],"tags":["ssrf","security","valueerror","network"],"backgroundTag":null,"analyzedSha":"e32fa9a52eab3b61ad7a45399bfde59b3e580fc4","analyzedAt":"2026-08-14T18:42:09.092Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}