Skip to content

🐛 fix(proxy): correctly classify IPv6 transition addresses in upstream validation - #4553

Merged
ReneWerner87 merged 3 commits into
mainfrom
claude/proxy-ssrf-ipv6-bypass-v5synf
Jul 22, 2026
Merged

ReneWerner87 merged 3 commits into
mainfrom
claude/proxy-ssrf-ipv6-bypass-v5synf

Conversation

@gaby

@gaby gaby commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

By default the proxy middleware only connects upstreams to public addresses and refuses non-public ones (loopback, unspecified, RFC 1918 private, link-local, multicast, and RFC 6598 CGNAT). That decision is made in isBlockedIP.

Several IPv6 "transition" formats embed an IPv4 address inside an IPv6 literal (6to4, Teredo, the NAT64 prefixes, ISATAP, SIIT, and the IPv4-compatible/mapped forms). isBlockedIP only recognized two of them — IPv4-compatible ::a.b.c.d and the NAT64 well-known prefix 64:ff9b::/96 — so the remaining forms were classified inconsistently with the IPv4 they wrap. For example ::ffff:0:7f00:1 and 2002:7f00:1:: both encode 127.0.0.1, yet were treated as ordinary public IPv6 while the bare 127.0.0.1 was correctly refused.

This PR makes the classifier handle the full set of well-known, fixed-prefix embeddings so the same result is produced regardless of which IPv6 form is used to write the address.

Changes introduced

  • Added isBlockedIPv6TransitionRange to reject the deprecated / local-use ranges in their entirety, where the embedded IPv4 offset is operator-dependent and there is no legitimate use as an upstream: 6to4 2002::/16 (RFC 3056, deprecated by RFC 7526), Teredo 2001:0000::/32 (RFC 4380), and the NAT64 local-use prefix 64:ff9b:1::/48 (RFC 8215, local-use only).
  • Extended embeddedIPv4 to unwrap the embedded IPv4 (and re-apply the same classification) for the forms whose offset is fixed and unambiguous and may legitimately be public: NAT64 well-known prefix 64:ff9b::/96 (RFC 6052), ISATAP (RFC 5214), SIIT IPv4-translated ::ffff:0:0/96 (RFC 2765), and IPv4-compatible ::a.b.c.d (RFC 4291). The IPv4-mapped form ::ffff:a.b.c.d continues to be handled directly since net.IP.To4 surfaces it.
  • Extended Test_Security_IsBlockedIP with cases for every form above — non-public embeddings are refused, public embeddings (e.g. 8.8.8.8 wrapped as 6to4/NAT64/ISATAP/SIIT) remain allowed.

Behavior note: an upstream literal in the wholesale-blocked ranges (6to4, Teredo, NAT64 local-use) that wraps a public IPv4 is now refused. These are deprecated tunneling / local-use ranges that do not appear as real upstream targets, so this should not affect legitimate usage.

  • Benchmarks: no change to hot paths; classification is a handful of extra byte comparisons on the existing validation path.
  • Documentation Update: no user-facing API change.
  • Changelog/What's New: worth a line under bug fixes.
  • Migration Guide: not needed.
  • API Alignment with Express: n/a.
  • API Longevity: no signature changes; internal helpers only.
  • Examples: n/a.

Type of change

  • Code consistency (non-breaking change which improves code reliability and robustness)

Checklist

  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Added or updated unit tests to validate the effectiveness of the changes.
  • Ensured that new and existing unit tests pass locally with the changes (go test ./middleware/proxy/, including -race).
  • No new dependencies introduced.

🤖 Generated with Claude Code

claude added 3 commits July 22, 2026 03:21
The proxy SSRF guard unwrapped IPv4-compatible and NAT64 well-known
prefix IPv6 addresses to their embedded IPv4 before applying the
blocklist, but missed three other IPv6 transition families that also
embed an IPv4 target inside a globally-routable IPv6 literal:

- 6to4 (2002::/16, RFC 3056): IPv4 gateway in bytes [2:6]
- Teredo (2001:0000::/32, RFC 4380): client IPv4 in bytes [12:16], inverted
- NAT64 local-use prefix (64:ff9b:1::/48, RFC 8215): IPv4 in bytes [12:16]

An attacker able to influence the proxy upstream URL could encode a
private IPv4 (e.g. 169.254.169.254 or 10.0.0.1) in one of these forms to
bypass isBlockedIP and reach internal infrastructure (CWE-918). Extend
embeddedIPv4 to unwrap all three so the same IPv4 blocklist applies, and
add coverage for each family in Test_Security_IsBlockedIP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBSS2Bm2KRAZmL2A5XfDhu
Address code-review gaps in the previous transition-address fix. The
per-form single-offset unwrapping left residual bypasses because several
transition families embed their IPv4 at an operator-dependent offset:

- NAT64 local-use 64:ff9b:1::/48 (RFC 8215) embeds IPv4 anywhere from a
  /32 to /96 boundary per RFC 6052; unwrapping only the trailing 32 bits
  missed the standard /48 embedding.
- Teredo 2001:0000::/32 (RFC 4380) can hide a private IPv4 in the
  un-obfuscated server field (bytes 4:8), not just the client field.

Block these deprecated tunneling / local-use ranges wholesale at the
CIDR level (6to4 2002::/16, Teredo 2001:0000::/32, NAT64 local-use
64:ff9b:1::/48) via isBlockedIPv6TransitionRange, matching Gitea's and
Coder's SSRF guards. None has a legitimate use as a proxy upstream, and
RFC 8215 §3 forbids the NAT64 local-use prefix on the public Internet.

Keep per-embedded-IP unwrapping only for wrappers whose embedded IPv4 is
unambiguous and may legitimately be public: NAT64 well-known prefix
(64:ff9b::/96, fixed /96 embedding) and IPv4-compatible ::a.b.c.d. Add
ISATAP (RFC 5214) unwrapping, which has no dedicated range to block.

Update Test_Security_IsBlockedIP: public-embedded 6to4/Teredo/NAT64-local
literals are now blocked wholesale, and add Teredo server-field and
ISATAP (private, public, global-scope u/g bit) coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBSS2Bm2KRAZmL2A5XfDhu
A code-review pass surfaced a residual smuggling vector in the same
fixed-offset embedded-IPv4 class the guard already covers: the deprecated
SIIT IPv4-translated form ::ffff:0:0/96 (RFC 2765). Its 0xffff marker sits
in bytes [8:10] (not [10:12] like the IPv4-mapped form), so net.IP.To4
does not surface it and embeddedIPv4 did not unwrap it — leaving e.g.
::ffff:0:7f00:1 (127.0.0.1) and ::ffff:0:a9fe:a9fe (169.254.169.254)
reachable past isBlockedIP.

Extend embeddedIPv4 to unwrap the trailing 32 bits of ::ffff:0:0/96 and
apply the IPv4 blocklist, consistent with the existing NAT64 well-known
prefix, ISATAP, and IPv4-compatible handling. Add coverage to
Test_Security_IsBlockedIP for private/metadata (blocked) and public
(allowed) translated literals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBSS2Bm2KRAZmL2A5XfDhu
@gaby
gaby requested a review from a team as a code owner July 22, 2026 11:22
@ReneWerner87 ReneWerner87 added this to v3 Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The proxy SSRF blocklist now blocks additional IPv6 transition ranges, unwraps more fixed-offset embedded IPv4 forms, and recursively validates extracted addresses. Tests expand coverage for NAT64, 6to4, Teredo, ISATAP, and SIIT variants.

Changes

SSRF IP blocklist

Layer / File(s) Summary
IPv6 transition blocking and validation
middleware/proxy/security.go, middleware/proxy/security_test.go
isBlockedIP blocks deprecated or local-use IPv6 transition ranges, recognizes additional fixed-offset IPv4 wrappers, recursively checks embedded IPv4 addresses, and adds corresponding block/allow test cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • gofiber/fiber#4405: Updates the same proxy SSRF blocking logic and IPv4/IPv6 special-prefix handling.
  • gofiber/fiber#4518: Modifies related NAT64 and IPv6 embedded-address unwrapping behavior and tests.

Suggested reviewers: renewerner87

Poem

I’m a rabbit guarding the IP gate,
Blocking tricky tunnels before they navigate.
Six-to-four and Teredo now meet the wall,
Wrapped IPv4 addresses are checked too, all.
Binky, binky—safer routes for all!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the proxy fix and accurately reflects the IPv6 transition address classification change.
Description check ✅ Passed The description follows the template well and covers the problem, changes, type, and checklist with only minor non-critical gaps.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/proxy-ssrf-ipv6-bypass-v5synf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ReneWerner87 ReneWerner87 added this to the v3 milestone Jul 22, 2026
@gaby gaby changed the title fix(proxy): block 6to4/Teredo/NAT64-local IPv6 SSRF bypass fix(proxy): Handlibg of IPv6 addresses Jul 22, 2026
@gaby gaby changed the title fix(proxy): Handlibg of IPv6 addresses fix(proxy): correctly classify IPv6 transition addresses in upstream validation Jul 22, 2026
@gaby gaby changed the title fix(proxy): correctly classify IPv6 transition addresses in upstream validation 🐛 fix(proxy): correctly classify IPv6 transition addresses in upstream validation Jul 22, 2026
@ReneWerner87
ReneWerner87 merged commit 9df1887 into main Jul 22, 2026
23 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/proxy-ssrf-ipv6-bypass-v5synf branch July 22, 2026 11:49
@github-project-automation github-project-automation Bot moved this to Done in v3 Jul 22, 2026
@ReneWerner87 ReneWerner87 modified the milestones: v3, v3.5.0 Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants