Repository navigation
fix(security): classify NAT64/IPv6-tunnelled IPv4 targets as non-public - #254
Conversation
is_public_ip() unwrapped ipv4_mapped but not the other IPv6 forms that carry an IPv4 target in their low 32 bits: - 64:ff9b::/96 (NAT64 well-known prefix, RFC 6052) - 64:ff9b:1::/48 (local-use, RFC 8215 - never a public destination) - ::/96 (deprecated IPv4-compatible form, RFC 4291) so '64:ff9b::127.0.0.1' and '::169.254.169.254' passed .is_global and defeated the SSRF guard that SafeHttpClient/PublicResolver rely on (CVE-2026-85740 bypass class). Classify the embedded IPv4 address instead; the address used for connecting is unchanged. Tests: 6 non-public classification cases, 1 NAT64 public-control case (DNS64 answer for a public target stays usable), and a resolver-level case asserting getaddrinfo returning a NAT64 tunnel to metadata is rejected. All 5 tunnel cases fail without this change.
CI note:
|
|
@Zongwei9888 — small follow-up on this PR. The one red check ( As a fork contributor I do not have permission to re-run workflows in this repo — would you mind hitting "Re-run failed jobs" on this run? https://github.com/HKUDS/DeepCode/actions/runs/37660317969 If the lock/runner-image conflict is still present it will fail identically, in which case it needs a lock update on main; happy to prepare a minimal fix PR for that if it would help. |
Since the 2026-10-04 windows-2022 image, the runner's Python carries a preinstalled pipx 1.17.11 that needs filelock>=4.0.9, packaging>=26.3 and platformdirs>=4.12.2. Installing the CI lock downgrades all three, so `pip check` failed in "Install package and test tools" before any test ran, on main and on every pull request. The job now creates a venv first and puts it on PATH; the install, pip check and test steps are unchanged. Reproduced locally with pipx 1.17.11 in a Python 3.12 (same three conflicts), and the fresh environment passes pip check and the Windows job's test files. Diagnosed by the authors of #254 and #257.
…gets as non-public `is_public_ip()` unwrapped only IPv4-mapped addresses, so `64:ff9b::127.0.0.1`, `::169.254.169.254` and a DNS64 answer such as `64:ff9b::a9fe:a9fe` passed as public. Addresses in the NAT64 well-known prefix and in the deprecated IPv4-compatible form are now classified by their embedded IPv4 address, and the RFC 8215 local-use prefix is never public. DNS64 answers for public IPv4 addresses still pass. Repair: drop an advisory ID that has no public record from a comment. Contributed by raymondginger2018-sudo.
|
Merged in c29a42a. Thanks for this, and for tracking down the Windows CI failure. What I checked on main before merging: One repair on top of your commit (c561d00): I removed the CVE-2026-85740 reference from the comment in A note for the record: the three new Your CI diagnosis was right: the |
Summary
is_public_ip()incore/network/safe_http.pyunwrapsipv4_mappedIPv6 addresses before classifying them, but not the other IPv6 forms that carry an IPv4 target in their low 32 bits:64:ff9b::/96— NAT64 well-known prefix (RFC 6052)64:ff9b:1::/48— NAT64 local-use prefix (RFC 8215; never a public destination)::/96— the deprecated IPv4-compatible form (RFC 4291)So a request to
http://[64:ff9b::127.0.0.1]/orhttp://[::169.254.169.254]/passes.is_global— the embedded loopback / link-local metadata address is never examined — and defeats the SSRF guard thatSafeHttpClientandPublicResolverboth rely on. This is the CVE-2026-85740 bypass class: tunnel an internal IPv4 target through a NAT64 well-known prefix so the address classifier sees a "global" IPv6 address instead of the IPv4 target.Changes
core/network/safe_http.py— add three pre-compiled networks (_NAT64_WELL_KNOWN,_NAT64_LOCAL_USE,_IPV4_COMPATIBLE). Inis_public_ip(),64:ff9b:1::/48is rejected outright (RFC 8215 local-use is never a public destination); for the other two prefixes the embedded IPv4 address (low 32 bits) is classified instead of the IPv6 address itself. The existingipv4_mappedbranch is untouched, and the address actually used for connecting is unchanged — only the public/non-public classification changes.tests/test_safe_http.py— three new non-public URL cases (http://[64:ff9b::127.0.0.1]/,http://[64:ff9b::169.254.169.254]/,http://[::127.0.0.1]/), six newtest_public_ip_classificationcases including the public control("64:ff9b::808:808", True)(a DNS64 answer for public8.8.8.8must stay usable), and a resolver-level case that monkeypatchesgetaddrinfoto return64:ff9b::169.254.169.254and assertsPublicResolverrejects it withOSError ... non-public.Evidence (before/after, this machine)
tests/test_safe_http.py36 passed.tests/test_web_tools.py(usesSafeHttpClientthroughcore/harness/tools/web.py) 11 passed.64:ff9b::808:808still classifies as public, so DNS64-resolved public targets keep working.uvx ruff@0.15.21 checkanduvx ruff@0.15.21 format --checkboth clean (the pinned revision from.pre-commit-config.yaml).Only
is_public_ip()classification changes; no new dependencies (stdlibipaddressonly) and no change to how requests are dispatched.