fix(url_safety): allow NAT64 well-known prefix (RFC 6052) - #12256
fix(url_safety): allow NAT64 well-known prefix (RFC 6052)#12256SergoVashakmadze wants to merge 1 commit into
Conversation
`ipaddress.IPv6Address.is_reserved` returns True for the NAT64 well-known prefix `64:ff9b::/96`. The existing `_is_blocked_ip` check treats any reserved IPv6 as an SSRF target and fails closed, but NAT64 addresses are the IPv6 representation of **public** IPv4 services reachable via an upstream NAT64 translator — blocking them is a false positive SSRF rejection. This hits users on: - Consumer ISPs running DS-Lite / B4 (dual-stack lite) - 464XLAT cellular networks (most modern LTE carriers) - CLAT'd systems on IPv6-only networks Symptom: `web_extract` fails with "Blocked: URL targets a private or internal network address" for legitimate public URLs, with no obvious recourse for the user. Fix: explicit allow for `64:ff9b::/96` before the generic is_private/is_reserved check. The check runs BEFORE the existing `_TRUSTED_PRIVATE_IP_HOSTS` allowlist so it applies broadly rather than per-host. Tests: - 5 new parametrized IPs in TestIsBlockedIp.test_allowed_ips covering the full `64:ff9b::/96` range - `test_nat64_url_allowed_end_to_end` — dual-stack hostname returning both an IPv4 AAAA and a NAT64 IPv6 - `test_nat64_only_resolution_allowed` — IPv6-only resolution returning only a NAT64 address All 61 tests in the file pass. Discovered while building a Hermes Agent hackathon submission (github.com/SergoVashakmadze/IoMarkets-Hermes) — web_extract kept erroring on blogs.nvidia.com, sec.gov, investor.nvidia.com because they resolved to NAT64 IPv6 on a consumer dual-stack-lite ISP.
|
Salvaged onto current main as #51758 with credit to you, @SergoVashakmadze. The NAT64 false-positive block is still live on main. One hardening change worth flagging: your version returns |
… (RFC 6052) Salvage of NousResearch#12256 by @SergoVashakmadze, re-targeted onto current main and hardened against a fail-open SSRF bypass. Problem: ipaddress.is_reserved flags the entire NAT64 Well-Known Prefix (64:ff9b::/96, RFC 6052), so is_safe_url() blocked every host that resolves to a NAT64 synthesized IPv6 address. On dual-stack-lite consumer ISPs that is a false positive for PUBLIC IPv4 targets reachable via the upstream NAT64 translator. Fix: instead of blanket-allowing the prefix (the original PR returned False unconditionally for any 64:ff9b:: address), extract the embedded IPv4 from the low 32 bits and re-run the existing IPv4 SSRF policy on it. Public embedded targets pass; private/loopback/link-local/reserved/CGNAT/metadata embedded targets (e.g. 64:ff9b::169.254.169.254, 64:ff9b::10.0.0.1) stay blocked. This mirrors main's existing ipv4_mapped (::ffff:x.x.x.x) handling via a shared _embedded_ipv4_is_blocked() helper. Testing: tests/tools/test_url_safety.py - 134 passed. Added public-NAT64 allow cases (fail without the fix), private/metadata-embedded NAT64 block cases (the fail-open guard), and three end-to-end is_safe_url() cases including a metadata-embedding host that must be rejected.
|
Superseded by #51758, which re-targets this onto current main and closes a fail-open SSRF gap in my original: I returned |
… (salvage of NousResearch#12256 by @SergoVashakmadze) Rebuilt on latest main (Bartok9 hygiene 2026-08-01). Original: NousResearch#51758
Problem
is_safe_urlfails closed on legitimate public URLs when the client machine is on a NAT64-aware network (dual-stack lite, 464XLAT cellular, or any IPv6-only with CLAT).Python's
IPv6Address.is_reservedreturns True for the entire NAT64 Well-Known Prefix64:ff9b::/96(RFC 6052). The existing_is_blocked_ipcheck treats any reserved IPv6 as an SSRF target, soweb_extractrejects those URLs with:But NAT64 addresses are the IPv6 representation of public IPv4 services reachable via an upstream NAT64 translator — the embedded IPv4 address in the low 32 bits is public. Blocking them is a false positive.
How I hit it
Consumer dual-stack-lite ISP returns both a public IPv4 and a NAT64 synthesized IPv6 for any AAAA lookup. e.g.
blogs.nvidia.com:is_safe_urliterates addresses and bails on the first blocked one, so the NAT64 entry short-circuits the public IPv4.Affects
sec.gov,investor.nvidia.com,blogs.nvidia.com, Google/Cloudflare DNS, and countless others depending on network topology.Fix
Explicit allow for
64:ff9b::/96at the top of_is_blocked_ip, before the genericis_private / is_reservedcheck. Mirrors the existing_CGNAT_NETWORKexplicit handling.Security posture is preserved: the NAT64 translator itself is a public gateway, so an attacker using a NAT64 IPv6 to reach an internal IPv4 would need to get Hermes onto the NAT64 operator's network AND configure their private IPv4 into the NAT64 mapping — infeasible in the threat model the SSRF check is defending against. (Truly internal IPv4s behind CGNAT/VPN are separately blocked via
_CGNAT_NETWORKand the standard private ranges.)Tests
All 61 tests in
tests/tools/test_url_safety.pypass, including new coverage:TestIsBlockedIp.test_allowed_ips— 5 new parametrized IPs across the64:ff9b::/96range (nvidia, Google DNS, Cloudflare, boundary cases)test_nat64_url_allowed_end_to_end— dual-stack hostname returning both a public IPv4 and a NAT64 IPv6test_nat64_only_resolution_allowed— IPv6-only resolution returning only a NAT64 address (DS-Lite scenario)Ran with:
python -m pytest tests/tools/test_url_safety.py -x # 61 passed in 1.33sContext
Found while building a Hermes Agent hackathon submission —
web_extractkept erroring on legitimate public URLs because the dev machine is on a DS-Lite home ISP. Spent a bit figuring out this wasn't a Firecrawl auth / quota issue. Hopefully saves someone else the debugging.Repo of the project that surfaced this: https://github.com/SergoVashakmadze/IoMarkets-Hermes (Hermes Creative Hackathon 2026).