Skip to content

fix(fetch): recognize NAT64 and 6to4 IPv6 forms in the SSRF private-IP guard#120

Open
dmchaledev wants to merge 1 commit into
mainfrom
claude/nice-mendel-ep0o5z
Open

fix(fetch): recognize NAT64 and 6to4 IPv6 forms in the SSRF private-IP guard#120
dmchaledev wants to merge 1 commit into
mainfrom
claude/nice-mendel-ep0o5z

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Summary

Fixes #118.

isPrivateIPv6 (src/fetch.ts) previously only recognized a handful of literal string prefixes: ::, ::1, fe80:, fc/fd, and the dotted-decimal form of IPv4-mapped addresses (::ffff:a.b.c.d). It did not recognize:

  • The NAT64 Well-Known Prefix 64:ff9b::/96 (RFC 6052), which is transparently translated to the embedded IPv4 address on IPv6-only networks running NAT64/464XLAT (the default IPv6-only mode on several cellular carriers, and a documented migration path for some cloud IPv6-only node pools).
  • 6to4 2002::/16, which embeds an IPv4 address the same way.
  • Non-dotted (all-hex) textual forms of IPv4-mapped addresses, e.g. ::ffff:7f00:1 instead of ::ffff:127.0.0.1 — the old regex only matched the dotted-decimal spelling.

Since assertPublicUrl relies entirely on isPrivateIPv6 to judge whether a resolved IPv6 address is safe to fetch, an attacker who controls DNS for a scan target (the exact untrusted-URL threat model this guard exists for) could return an address like 64:ff9b::a9fe:a9fe — which embeds the cloud metadata IP 169.254.169.254 — and have it sail past the check on an affected network, even though the plain-v4 form of the same host is already correctly blocked.

Fix

Rather than adding another one-off regex per prefix, this replaces the ad hoc string matching with a general IPv6 address expander (expandIPv6) that parses any syntactically valid IPv6 literal (handling :: compression and a trailing embedded-IPv4 dotted-decimal suffix) into its 8 16-bit groups. isPrivateIPv6 then checks the expanded groups against:

  • IPv4-mapped (::ffff:0:0/96)
  • NAT64 (64:ff9b::/96)
  • 6to4 (2002::/16)

and recurses into the existing isPrivateIPv4 check on the embedded address in each case — mirroring how ::ffff: was already handled, just generalized. If the address can't be confidently parsed, it now fails closed (true), consistent with the fail-closed behavior isPrivateOrReservedIP already uses for unclassifiable addresses.

Test plan

  • Added regression tests in test/fetch.test.ts:
    • NAT64 address embedding the metadata IP (both hex-group and mixed dotted-decimal notation)
    • NAT64 address embedding an RFC1918 address
    • 6to4 address embedding loopback
    • NAT64 address embedding a genuinely public IP is still allowed through
    • IPv4-mapped address in non-dotted (all-hex) notation is still rejected
  • npm test — 154/154 passing
  • npm run typecheck — clean
  • npm run build — clean

Generated by Claude Code

…P guard

isPrivateIPv6 only matched loopback/link-local/ULA prefixes and the dotted-decimal
form of IPv4-mapped addresses, so an attacker-controlled DNS response using the
NAT64 Well-Known Prefix (64:ff9b::/96, RFC 6052) or 6to4 (2002::/16) could embed
a private/metadata IPv4 address (e.g. 169.254.169.254) and slip past the check
on networks that transparently translate those prefixes. Replace the regex
prefix-matching with a general IPv6 group expander so embedded-IPv4 forms are
checked consistently regardless of textual notation (dotted-decimal or hex).

Fixes #118.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ew2xqjd45ft3s8KpVT8Vt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSRF guard's isPrivateIPv6 doesn't recognize the NAT64 well-known prefix — private IPv4 targets can be reached via a synthesized IPv6 literal

2 participants