{"record":{"id":"d787b98eff50482b","repo":"aio-libs/aiohttp","slug":"url-is-not-a-canonical-ipv4-address","errorCode":null,"errorMessage":"{url} - is not a canonical IPv4 address","messagePattern":"(.+?) - is not a canonical IPv4 address","errorType":"exception","errorClass":"InvalidUrlClientError","httpStatus":null,"severity":"error","filePath":"aiohttp/connector.py","lineNumber":1131,"sourceCode":"    def clear_dns_cache(self, host: str | None = None, port: int | None = None) -> None:\n        \"\"\"Remove specified host/port or clear all dns local cache.\"\"\"\n        if host is not None and port is not None:\n            self._cached_hosts.remove((host, port))\n        elif host is not None or port is not None:\n            raise ValueError(\"either both host and port or none of them are allowed\")\n        else:\n            self._cached_hosts.clear()\n\n    async def _resolve_host(\n        self, host: str, port: int, traces: Sequence[\"Trace\"] | None = None\n    ) -> list[ResolveResult]:\n        \"\"\"Resolve host and return list of addresses.\"\"\"\n        if is_ip_address(host):\n            # Reject legacy numeric IPv4 forms (e.g. 2130706433, 127.1) that\n            # socket would map onto an address, slipping past a connector-level\n            # policy that only sees the raw host.\n            if \":\" not in host and not is_canonical_ipv4_address(host):\n                raise InvalidUrlClientError(host, \"is not a canonical IPv4 address\")\n            return [\n                {\n                    \"hostname\": host,\n                    \"host\": host,\n                    \"port\": port,\n                    \"family\": self._family,\n                    \"proto\": 0,\n                    \"flags\": 0,\n                }\n            ]\n\n        if not self._use_dns_cache:\n            if traces:\n                for trace in traces:\n                    await trace.send_dns_resolvehost_start(host)\n\n            if self._closed:\n                raise ClientConnectionError(\"Connector is closed\")","sourceCodeStart":1113,"sourceCodeEnd":1149,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/connector.py#L1113-L1149","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nawait session.get('http://2130706433/admin')\n# after - normalize first\nimport ipaddress\nhost = '2130706433'\ntry:\n    canon = str(ipaddress.IPv4Address(int(host)))\nexcept ValueError:\n    canon = host  # leave DNS name alone\nawait session.get(f'http://{canon}/admin')","handlingStrategy":"validation","validationCode":"import ipaddress\nfrom urllib.parse import urlparse\n\ndef canonical_host(url: str) -> str:\n    host = urlparse(url).hostname or ''\n    if host and host.replace('.', '').isdigit() and '.' not in host:\n        # integer-form IPv4 like 2130706433\n        return str(ipaddress.IPv4Address(int(host)))\n    return host\n\nhost = canonical_host(url)\nif host != urlparse(url).hostname:\n    raise ValueError(f'non-canonical IPv4 rejected: {url}')","typeGuard":"import ipaddress\n\ndef is_canonical_ipv4(host: str) -> bool:\n    try:\n        ipaddress.IPv4Address(host)\n    except (ipaddress.AddressValueError, ValueError):\n        return False\n    # reject shorthand forms: IPv4Address accepts '127.1' on some Pythons\n    return host.count('.') == 3 and all(p.isdigit() and 0 <= int(p) <= 255 for p in host.split('.'))","tryCatchPattern":"try:\n    resp = await session.get(url)\nexcept aiohttp.InvalidUrlClientError as exc:\n    if 'canonical IPv4' in str(exc):\n        raise ValueError(f'reject ambiguous IP form: {url}') from exc\n    raise","preventionTips":["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."],"tags":["url","security","ssrf","ip-validation","invalid-url"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}