Skip to content

Block NAT64 and IPv4-compatible embedded addresses in load_web_page SSRF guard - #6486

Open
herdiyana256 wants to merge 3 commits into
google:mainfrom
herdiyana256:fix-load-web-page-nat64-ssrf
Open

Block NAT64 and IPv4-compatible embedded addresses in load_web_page SSRF guard#6486
herdiyana256 wants to merge 3 commits into
google:mainfrom
herdiyana256:fix-load-web-page-nat64-ssrf

Conversation

@herdiyana256

Copy link
Copy Markdown

load_web_page validates fetch targets through _is_blocked_address, which rejects any address that is not global. That correctly blocks IPv4 private, loopback, and link-local literals, hostnames that resolve to them, and the IPv4-mapped IPv6 form ::ffff:a.b.c.d.

It does not block two other IPv6 encodings that carry an internal IPv4 address. The NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) and the deprecated IPv4-compatible form ::a.b.c.d both embed an IPv4 address in their low 32 bits, but the standard library classifies them as globally routable, so is_global is True and the guard allows them. As a result 64:ff9b::169.254.169.254 and ::169.254.169.254 pass the check, and the same holds for private and loopback ranges.

On a NAT64-enabled network (common in IPv6-only environments) a connection to 64:ff9b::169.254.169.254 is translated by the NAT64 gateway to the IPv4 link-local metadata endpoint 169.254.169.254, so an attacker who can influence the URL passed to the tool can reach cloud metadata or other internal services despite the guard. The target can also be delivered through DNS by publishing an AAAA record inside the prefix.

This change unwraps the embedded IPv4 address from the IPv4-mapped, NAT64 well-known, and IPv4-compatible forms and re-checks it, so a target is blocked when either the IPv6 address or its embedded IPv4 address is non-global. NAT64 to a genuinely public IPv4 such as 64:ff9b::8.8.8.8 is still allowed, and the existing load_web_page test suite continues to pass.

@adk-bot adk-bot added the tools [Component] This issue is related to tools label Jul 26, 2026
…SRF guard

_is_blocked_address rejected any address that is not global, which correctly
blocks IPv4 private/loopback/link-local literals and their IPv4-mapped IPv6
form. It did not block the NAT64 well-known prefix (64:ff9b::/96) or the
deprecated IPv4-compatible form (::a.b.c.d): ipaddress classifies both as
globally routable even though they embed an internal IPv4 address. On a
NAT64-enabled network a request to 64:ff9b::169.254.169.254 is translated to
the IPv4 link-local metadata endpoint, so the guard could be bypassed to reach
internal or metadata addresses.

Unwrap the embedded IPv4 address from these forms and re-check it, so a target
is blocked when either the IPv6 address or its embedded IPv4 address is
non-global. NAT64 to a genuinely public IPv4 (for example 64:ff9b::8.8.8.8)
is still allowed.
@rohityan
rohityan force-pushed the fix-load-web-page-nat64-ssrf branch from 602a5c7 to 4cc8fed Compare July 27, 2026 21:24
@rohityan rohityan added the request clarification [Status] The maintainer need clarification or more information from the author label Jul 27, 2026
@rohityan

Copy link
Copy Markdown
Collaborator

Hi @herdiyana256 , Thank you for your contribution! We appreciate you taking the time to submit this pull request.
Can you also add a unit test and fix the failing CI checks.

@herdiyana256

Copy link
Copy Markdown
Author

Thanks @rohityan! Added unit tests in tests/unittests/tools/test_load_web_page.py:

  • test_is_blocked_address_blocks_embedded_internal_ipv4 / test_is_blocked_address_allows_embedded_public_ipv4 — parametrized over the NAT64 well-known prefix (64:ff9b::/96), the deprecated IPv4-compatible form (::a.b.c.d) and the IPv4-mapped form: blocked when the embedded IPv4 is internal (loopback / 169.254.169.254 / private), allowed when it is public.
  • End-to-end load_web_page cases for a NAT64-wrapped metadata literal, an IPv4-compatible loopback literal, and a hostname resolving (via getaddrinfo) to a NAT64-wrapped 169.254.169.254 — all confirm the request is refused before any socket send.

All pass locally (pytest tests/unittests/tools/test_load_web_page.py), CLA/checks green. PTAL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

request clarification [Status] The maintainer need clarification or more information from the author tools [Component] This issue is related to tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants