Skip to content

fix(ssrf): block 6to4 and NAT64 IPv6 wrappers of private IPv4#13

Merged
iaj6 merged 1 commit into
mainfrom
fix/ssrf-ipv6
Jun 9, 2026
Merged

fix(ssrf): block 6to4 and NAT64 IPv6 wrappers of private IPv4#13
iaj6 merged 1 commit into
mainfrom
fix/ssrf-ipv6

Conversation

@iaj6

@iaj6 iaj6 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

What

The SSRF guard's isBlockedIpv6 didn't decode two IPv6 transition forms that embed an IPv4 address, so an attacker could smuggle a private/reserved/metadata target past the v6 check:

Form Example Decodes to
6to4 (2002::/16, v4 in hextets 1–2) 2002:a9fe:a9fe:: 169.254.169.254 (cloud metadata)
2002:7f00:1:: 127.0.0.1
NAT64 well-known (64:ff9b::/96, v4 in low 32 bits) 64:ff9b::7f00:1 127.0.0.1

Both previously fell through to allowed.

Change

Decode the embedded IPv4 and range-check it via isBlockedIpv4 — consistent with the existing ::ffff: IPv4-mapped handling, so a 6to4/NAT64 wrapper of a public v4 (e.g. 2002:808:808:: → 8.8.8.8) stays allowed while a wrapper of a private/reserved v4 is blocked. A code comment documents the residual NAT64 network-specific-prefix limitation (an operator-chosen prefix can't be decoded generically; the pre-fetch DNS re-resolution is the backstop for hostnames).

The DNS-error fail-open in assertResolvesToPublic is deliberately unchanged (per decision).

Tests

6to4 + NAT64 wrappers of loopback/metadata/private (hex and dotted forms) now blocked; public wrappers still allowed; bracketed IP-literal hosts rejected by validateOutboundUrl. 1263 pass, lint clean, build green.

Review

Adversarial 2-lens (correctness false-neg/pos + completeness of other embedding vectors): 0 real problems — the reviewer executed the decode logic against 16 candidate addresses to confirm no bypass. Teredo / ISATAP / 6bone confirmed not reachable from a server-side fetch with no tunnel interface (deferred as hygiene); ISATAP's link-local form is already blocked.

🤖 Generated with Claude Code

isBlockedIpv6 didn't decode two IPv6 transition forms that embed an IPv4
address, so an attacker could smuggle a private/reserved/metadata target past
the v6 check:
  - 6to4 (2002::/16): embedded v4 in hextets 1-2, e.g. 2002:a9fe:a9fe:: →
    169.254.169.254 (cloud metadata), 2002:7f00:1:: → 127.0.0.1.
  - NAT64 well-known prefix (64:ff9b::/96): embedded v4 in the low 32 bits,
    e.g. 64:ff9b::7f00:1 → 127.0.0.1.
Both previously fell through to "allowed".

Decode the embedded IPv4 and range-check it via isBlockedIpv4 — consistent
with the existing ::ffff: IPv4-mapped handling, so a 6to4/NAT64 wrapper of a
PUBLIC v4 (e.g. 2002:808:808::) stays allowed while a wrapper of a
private/reserved v4 is blocked. A comment documents the residual NAT64
network-specific-prefix limitation (not generically decodable).

The DNS-error fail-open in assertResolvesToPublic is deliberately unchanged.

Tests: 6to4 + NAT64 wrappers of loopback/metadata/private (hex and dotted
forms) now blocked; public wrappers still allowed; bracketed IP-literal hosts
rejected by validateOutboundUrl. Full suite 1263 pass, lint clean, build green.

Review: adversarial 2-lens (correctness false-neg/pos + completeness of other
embedding vectors) — 0 real problems; Teredo/ISATAP/6bone confirmed not
reachable from a server fetch with no tunnel interface (deferred as hygiene).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@iaj6 iaj6 merged commit a308ea8 into main Jun 9, 2026
3 checks passed
@iaj6 iaj6 deleted the fix/ssrf-ipv6 branch June 9, 2026 14:49
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.

1 participant