aio-libs/aiohttp · error · InvalidUrlClientError

{url} - is not a canonical IPv4 address

Error message

{url} - is not a canonical IPv4 address

What it means

Raised inside _resolve_host when the host is a numeric IPv4 that is_ip_address accepts but is_canonical_ipv4_address rejects. Legacy numeric forms like a plain integer (2130706433), dotted shorthand (127.1), or octal/hex variants would be silently mapped to an address by the socket layer, bypassing any connector-level policy that only inspects the raw host string. aiohttp refuses them with InvalidUrlClientError to close the SSRF / policy-bypass vector.

Source

Thrown at aiohttp/connector.py:1131

    def clear_dns_cache(self, host: str | None = None, port: int | None = None) -> None:
        """Remove specified host/port or clear all dns local cache."""
        if host is not None and port is not None:
            self._cached_hosts.remove((host, port))
        elif host is not None or port is not None:
            raise ValueError("either both host and port or none of them are allowed")
        else:
            self._cached_hosts.clear()

    async def _resolve_host(
        self, host: str, port: int, traces: Sequence["Trace"] | None = None
    ) -> list[ResolveResult]:
        """Resolve host and return list of addresses."""
        if is_ip_address(host):
            # Reject legacy numeric IPv4 forms (e.g. 2130706433, 127.1) that
            # socket would map onto an address, slipping past a connector-level
            # policy that only sees the raw host.
            if ":" not in host and not is_canonical_ipv4_address(host):
                raise InvalidUrlClientError(host, "is not a canonical IPv4 address")
            return [
                {
                    "hostname": host,
                    "host": host,
                    "port": port,
                    "family": self._family,
                    "proto": 0,
                    "flags": 0,
                }
            ]

        if not self._use_dns_cache:
            if traces:
                for trace in traces:
                    await trace.send_dns_resolvehost_start(host)

            if self._closed:
                raise ClientConnectionError("Connector is closed")

View on GitHub (pinned to d041d4d0fd)

Solutions

  1. Canonicalize the host before the request: reject or rewrite ambiguous numeric forms to dotted-decimal.
  2. Validate user-supplied URLs with ipaddress.ip_address after normalizing, and reject anything that needed reinterpretation.
  3. Resolve through DNS names rather than allowing raw numeric IP literals from untrusted input.
  4. Treat InvalidUrlClientError at the boundary as a security event, not a transient error.

Example fix

# before
await session.get('http://2130706433/admin')
# after - normalize first
import ipaddress
host = '2130706433'
try:
    canon = str(ipaddress.IPv4Address(int(host)))
except ValueError:
    canon = host  # leave DNS name alone
await session.get(f'http://{canon}/admin')
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress
from urllib.parse import urlparse

def canonical_host(url: str) -> str:
    host = urlparse(url).hostname or ''
    if host and host.replace('.', '').isdigit() and '.' not in host:
        # integer-form IPv4 like 2130706433
        return str(ipaddress.IPv4Address(int(host)))
    return host

host = canonical_host(url)
if host != urlparse(url).hostname:
    raise ValueError(f'non-canonical IPv4 rejected: {url}')

Type guard

import ipaddress

def is_canonical_ipv4(host: str) -> bool:
    try:
        ipaddress.IPv4Address(host)
    except (ipaddress.AddressValueError, ValueError):
        return False
    # reject shorthand forms: IPv4Address accepts '127.1' on some Pythons
    return host.count('.') == 3 and all(p.isdigit() and 0 <= int(p) <= 255 for p in host.split('.'))

Try / catch

try:
    resp = await session.get(url)
except aiohttp.InvalidUrlClientError as exc:
    if 'canonical IPv4' in str(exc):
        raise ValueError(f'reject ambiguous IP form: {url}') from exc
    raise

Prevention

When it happens

Trigger: Passing url='http://2130706433/' (integer form for 127.0.0.1); url='http://127.1/'; octal 'http://0177.0.0.1/'; hex 'http://0x7f.1/'; a redirect Location that uses an ambiguous form to evade an allow-list.

Common situations: SSRF probes against a service that fronts internal IPs; user-supplied URLs that need to be canonicalized before fetch; buggy URL builders that strip leading zeros; redirects from compromised origins.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/d787b98eff50482b. Report an issue: GitHub.