Graphify-Labs/graphify · error · OSError

SSRF blocked: IP {addr} resolved from '{host}' is private/re

Error message

SSRF blocked: IP {addr} resolved from '{host}' is private/reserved

What it means

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.

Source

Thrown at graphify/security.py:173

# ---------------------------------------------------------------------------


def _resolve_and_validate(host: str, port: int) -> tuple[int, str]:
    """Resolve *host* once and return (family, validated_ip) for the first
    address that is not in a blocked range.

    Raises OSError if every resolved address is private/reserved/internal,
    matching the failure mode urllib/http.client expect from connect().
    """
    infos = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
    for family, _type, _proto, _canon, sockaddr in infos:
        addr = sockaddr[0]
        try:
            ip = ipaddress.ip_address(addr)
        except ValueError:
            continue
        if _ip_is_blocked(ip):
            raise OSError(
                f"SSRF blocked: IP {addr} resolved from '{host}' is private/reserved"
            )
        return family, addr
    raise OSError(f"SSRF blocked: no usable address resolved from '{host}'")


class _SSRFGuardedHTTPConnection(http.client.HTTPConnection):
    """HTTPConnection that resolves + validates DNS once, then connects to the
    exact validated IP (no second resolution = no DNS-rebind TOCTOU)."""

    def connect(self) -> None:
        family, ip = _resolve_and_validate(self.host, self.port)
        self.sock = socket.create_connection(
            (ip, self.port),
            self.timeout,
            self.source_address,
        )
        if self._tunnel_host:

View on GitHub (pinned to 7fe58b0b0f)

Solutions

  1. Point the URL at a genuinely public host (or the external IP your deployment exposes) instead of a private/loopback address.
  2. 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.
  3. Check DNS for the failing host (dig +short <host>) and confirm which record is triggering the block before changing anything.
  4. Never disable the SSRF guard in shared/server deployments; the block exists to stop internal-network probing.

Example fix

# before
html = safe_fetch_text("http://localhost:8080/docs/page.html")

# after: serve the content from a public host, or read local files directly
html = Path("docs/page.html").read_text(encoding="utf-8")
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress, socket

def resolves_to_public(host: str) -> bool:
    for *_rest, sockaddr in socket.getaddrinfo(host, 443, socket.AF_UNSPEC, socket.SOCK_STREAM):
        try:
            ip = ipaddress.ip_address(sockaddr[0])
        except ValueError:
            continue
        if not (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast):
            return True
    return False

assert resolves_to_public("example.com"), "host resolves only to blocked ranges"

Type guard

def is_safe_fetch_host(host: str) -> bool:
    try:
        return resolves_to_public(host)
    except socket.gaierror:
        return False

Try / catch

try:
    body = safe_fetch(url)
except OSError as e:
    if "SSRF blocked" in str(e):
        # policy decision: pick a different, public URL; do not disable the guard
        raise ValueError(f"Refusing internal target: {url}") from e
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Graphify-Labs/graphify@7fe58b0b0f (2026-08-14). Data as JSON: /api/errors/5a953f04b2619924. Report an issue: GitHub.