{"record":{"id":"5a953f04b2619924","repo":"Graphify-Labs/graphify","slug":"ssrf-blocked-ip-addr-resolved-from-host-is","errorCode":null,"errorMessage":"SSRF blocked: IP {addr} resolved from '{host}' is private/reserved","messagePattern":"SSRF blocked: IP (.+?) resolved from '(.+?)' is private/reserved","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"graphify/security.py","lineNumber":173,"sourceCode":"# ---------------------------------------------------------------------------\n\n\ndef _resolve_and_validate(host: str, port: int) -> tuple[int, str]:\n    \"\"\"Resolve *host* once and return (family, validated_ip) for the first\n    address that is not in a blocked range.\n\n    Raises OSError if every resolved address is private/reserved/internal,\n    matching the failure mode urllib/http.client expect from connect().\n    \"\"\"\n    infos = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)\n    for family, _type, _proto, _canon, sockaddr in infos:\n        addr = sockaddr[0]\n        try:\n            ip = ipaddress.ip_address(addr)\n        except ValueError:\n            continue\n        if _ip_is_blocked(ip):\n            raise OSError(\n                f\"SSRF blocked: IP {addr} resolved from '{host}' is private/reserved\"\n            )\n        return family, addr\n    raise OSError(f\"SSRF blocked: no usable address resolved from '{host}'\")\n\n\nclass _SSRFGuardedHTTPConnection(http.client.HTTPConnection):\n    \"\"\"HTTPConnection that resolves + validates DNS once, then connects to the\n    exact validated IP (no second resolution = no DNS-rebind TOCTOU).\"\"\"\n\n    def connect(self) -> None:\n        family, ip = _resolve_and_validate(self.host, self.port)\n        self.sock = socket.create_connection(\n            (ip, self.port),\n            self.timeout,\n            self.source_address,\n        )\n        if self._tunnel_host:","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/Graphify-Labs/graphify/blob/7fe58b0b0f3873be9a21c30106b8b8527c353aa6/graphify/security.py#L155-L191","documentation":"Raised by graphify's SSRF guard (_resolve_and_validate in graphify/security.py) when a hostname passed to safe_fetch/safe_fetch_text resolves to an IP that is private, loopback, link-local, or otherwise reserved. The library enforces this before opening any connection so that fetched URLs cannot reach internal network targets. It surfaces as an OSError to mimic a failed connect(), which urllib/http.client callers already expect.","triggerScenarios":"Calling safe_fetch or safe_fetch_text with a URL whose host resolves to 127.0.0.1, 10.x.x.x, 192.168.x.x, 172.16-31.x, 169.254.169.254 (cloud metadata), ::1, or IPv6 unique-local fc00::/7. Also triggered when a public hostname is DNS-rebound to an internal address at resolution time.","commonSituations":"Fetching documentation pages that reference localhost aliases; test environments where a domain in the config points at an internal box; environments with split-horizon DNS where the same name resolves internally to a private IP; accidentally passing http://localhost:8000/... style URLs to a fetch helper.","solutions":["Point the URL at a genuinely public host (or the external IP your deployment exposes) instead of a private/loopback address.","If the target is an internal service you own and the fetch path is trusted, use plain urllib/requests outside graphify's safe_fetch rather than weakening the guard.","Check DNS for the failing host (dig +short <host>) and confirm which record is triggering the block before changing anything.","Never disable the SSRF guard in shared/server deployments; the block exists to stop internal-network probing."],"exampleFix":"# before\nhtml = safe_fetch_text(\"http://localhost:8080/docs/page.html\")\n\n# after: serve the content from a public host, or read local files directly\nhtml = Path(\"docs/page.html\").read_text(encoding=\"utf-8\")","handlingStrategy":"validation","validationCode":"import ipaddress, socket\n\ndef resolves_to_public(host: str) -> bool:\n    for *_rest, sockaddr in socket.getaddrinfo(host, 443, socket.AF_UNSPEC, socket.SOCK_STREAM):\n        try:\n            ip = ipaddress.ip_address(sockaddr[0])\n        except ValueError:\n            continue\n        if not (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast):\n            return True\n    return False\n\nassert resolves_to_public(\"example.com\"), \"host resolves only to blocked ranges\"","typeGuard":"def is_safe_fetch_host(host: str) -> bool:\n    try:\n        return resolves_to_public(host)\n    except socket.gaierror:\n        return False","tryCatchPattern":"try:\n    body = safe_fetch(url)\nexcept OSError as e:\n    if \"SSRF blocked\" in str(e):\n        # policy decision: pick a different, public URL; do not disable the guard\n        raise ValueError(f\"Refusing internal target: {url}\") from e\n    raise","preventionTips":["Only pass public HTTPS URLs to safe_fetch/safe_fetch_text.","Never configure localhost/internal hostnames into fetch pipelines that run in shared or server contexts.","Pre-resolve and audit hosts in config at deploy time with a public-IP check."],"tags":["network","ssrf","security","dns"],"backgroundTag":null,"analyzedSha":"7fe58b0b0f3873be9a21c30106b8b8527c353aa6","analyzedAt":"2026-08-14T19:23:21.323Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}