Skip to content

fix(fetch): close NAT64/6to4 SSRF-guard bypass in isPrivateIPv6#119

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

fix(fetch): close NAT64/6to4 SSRF-guard bypass in isPrivateIPv6#119
dmchaledev wants to merge 1 commit into
mainfrom
claude/nice-mendel-wwqik1

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Summary

Fixes #118. isPrivateIPv6 (src/fetch.ts) only pattern-matched a handful of literal IPv6 prefixes (::, ::1, fe80:, fc/fd, ::ffff:a.b.c.d). It did not recognize the NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) or the 6to4 prefix 2002::/16, both of which embed a literal IPv4 address in the low bits.

An address like 64:ff9b::a9fe:a9fe embeds 169.254.169.254 (the cloud metadata endpoint) — a host this library already blocks in its plain IPv4 form via isPrivateIPv4. On a NAT64/464XLAT network (the default IPv6-only mode on several cellular carriers and some cloud node pools this library's stated ASM/scanning use case may run under), that literal is transparently routed to the real private/metadata IPv4 by the network stack, bypassing the SSRF guard entirely.

  • assertPublicUrlisPrivateOrReservedIPisPrivateIPv6 is the only place this decision is made for IPv6 results of dns.lookup(), so this closes the gap at its single point of control.
  • checkCSP/rules.ts and the rest of the analyzer are unaffected — this is scoped entirely to the private-IP guard in fetch.ts.

Approach

Rather than extending the prefix-matching with more fixed-shape regexes (which the original issue flagged as risky — RFC 5952 zero-run compression can swallow zero hextets from within the embedded IPv4, not just from the routing prefix, so a rigid 64:ff9b::(hex):(hex) pattern would miss e.g. 64:ff9b::101 for 0.0.1.1), this adds a small general-purpose ipv6ToBytes parser that expands any single-::-compressed address to its 16 raw bytes, then checks the 96/16-bit prefixes byte-for-byte and recurses the embedded IPv4 into the existing isPrivateIPv4 check — mirroring how ::ffff: (IPv4-mapped) is already handled.

Test plan

  • npm run typecheck — passes
  • npm test — all 155 tests pass, including 6 new cases in test/fetch.test.ts:
    • NAT64 embedding the metadata IP, loopback, and an RFC1918 address
    • a NAT64 address where zero-run compression swallows part of the embedded IPv4 (64:ff9b::1010.0.1.1)
    • 6to4 embedding the metadata IP
    • a NAT64 address embedding a public IPv4 (8.8.8.8) — confirms no over-blocking
    • an ordinary public IPv6 address (Google Public DNS) unrelated to either prefix — confirms no regression
  • npm run build — passes
  • npm audit --audit-level=high — 0 vulnerabilities

Generated by Claude Code

isPrivateIPv6 only pattern-matched a handful of literal IPv6 prefixes,
so an address under the NAT64 well-known prefix (64:ff9b::/96, RFC 6052)
or the 6to4 prefix (2002::/16) that embeds a private/metadata IPv4 (e.g.
64:ff9b::a9fe:a9fe for 169.254.169.254) passed through as "not private" —
even though the plain IPv4 form of the same host is already blocked.
NAT64/464XLAT is the default IPv6-only mode on several cellular carriers
and some cloud node pools, so this is reachable in real deployments of
the "scan a customer-supplied URL" use case this library advertises.

Replaces the fixed-shape regex approach with a general IPv6-to-bytes
parser so RFC 5952 zero-run compression — which can swallow zero
hextets from *within* the embedded address, not just the routing
prefix — doesn't produce a form a rigid pattern would miss.

Fixes #118
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