Graphify-Labs/graphify · error · OSError

SSRF blocked: no usable address resolved from '{host}'

Error message

SSRF blocked: no usable address resolved from '{host}'

What it means

Raised by _resolve_and_validate in graphify/security.py when getaddrinfo returned addresses but none of them were usable: every candidate was either blocked as private/reserved or was not a parseable IP (ValueError in ip_address, which is skipped). It is the 'no acceptable address left' counterpart to the single-IP block error, and like it is an OSError matching connect() failure semantics.

Source

Thrown at graphify/security.py:177

    """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:
            self._tunnel()


class _SSRFGuardedHTTPSConnection(http.client.HTTPSConnection):

View on GitHub (pinned to 7fe58b0b0f)

Solutions

  1. Verify the host actually resolves to a public address: run socket.getaddrinfo(host, 443) and inspect the returned IPs.
  2. If the host is intentionally internal, move the fetch outside safe_fetch to a transport you explicitly control.
  3. Fix or bypass the DNS record / search domain that is mapping the name onto private addresses.
  4. If you need graphify to fetch from an allow-listed internal mirror, request an explicit allowlist feature upstream instead of monkey-patching _ip_is_blocked.

Example fix

# before
try:
    data = safe_fetch("http://mirror.internal.example/pkg.json")
except OSError as e:
    raise

# after: diagnose what the name resolves to, use a public mirror or local file
import socket
print(socket.getaddrinfo("mirror.internal.example", 80))
data = Path("pkg.json").read_bytes()
Defensive patterns

Strategy: validation

Validate before calling

def has_usable_address(host: str) -> bool:
    """True if at least one resolved address is a public unicast IP."""
    try:
        infos = socket.getaddrinfo(host, 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
    except socket.gaierror:
        return False
    for *_rest, sockaddr in infos:
        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

Type guard

def is_fetchable_host(host: str) -> bool:
    return has_usable_address(host)

Try / catch

try:
    body = safe_fetch(url)
except OSError as e:
    if str(e).startswith("SSRF blocked: no usable address"):
        logger.warning("host %s has no public address; skipping", host)
        return None
    raise

Prevention

When it happens

Trigger: Calling safe_fetch/safe_fetch_text where a host resolves only to blocked ranges (e.g. a name with only 127.0.0.1 and ::1 A/AAAA records), or where getaddrinfo returns sockaddrs whose address strings ip_address cannot parse, so every entry is skipped and the loop falls through.

Common situations: Hostnames that are internal-only aliases resolving exclusively to RFC1918 space; hostnames resolving to non-IP sockaddr entries on unusual platform resolvers; typo'd host that a search-domain wildcard resolves to an internal catch-all address.

Related errors


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