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
- Canonicalize the host before the request: reject or rewrite ambiguous numeric forms to dotted-decimal.
- Validate user-supplied URLs with ipaddress.ip_address after normalizing, and reject anything that needed reinterpretation.
- Resolve through DNS names rather than allowing raw numeric IP literals from untrusted input.
- 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
- Normalize user-supplied URLs through ipaddress before fetching.
- Treat ambiguous numeric IPs from redirects or user input as a security event.
- Resolve through DNS names for untrusted input rather than allowing raw numeric forms.
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
- 1009
- 1009
- base_url must have a trailing '/'
- Cannot combine AUTHORIZATION header with credentials encoded
- Invalid challenge response
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/d787b98eff50482b.
Report an issue: GitHub.