Skip to content

SSRF: block NAT64 and 6to4 IPv6 encapsulation ranges#2987

Closed
jeremy wants to merge 1 commit into
mainfrom
security/ssrf-ipv6-encapsulation-ranges
Closed

SSRF: block NAT64 and 6to4 IPv6 encapsulation ranges#2987
jeremy wants to merge 1 commit into
mainfrom
security/ssrf-ipv6-encapsulation-ranges

Conversation

@jeremy

@jeremy jeremy commented Jul 20, 2026

Copy link
Copy Markdown
Member

What

SsrfProtection's DISALLOWED_IP_RANGES covered CGNAT (100.64.0.0/10) and benchmark (198.18.0.0/15) but missed the IPv6 encapsulation ranges NAT64 64:ff9b::/96 and 6to4 2002::/16. These embed an IPv4 address (including private space) in an IPv6 address that private?/link_local? do not catch.

Found via weekend run #3873122 (once-campfire OpenGraph-unfurl SSRF); confirmed latent across bc3/fizzy/once-campfire.

Fix

Add NAT64 and 6to4 to DISALLOWED_IP_RANGES. blocked_address? already applies to every resolved address; the DNS resolution path (resolve_public_ip, which already reads all A/AAAA records) is unchanged.

Tests

test/models/ssrf_protection_test.rb: blocked_address? is true for 64:ff9b::a00:1 and 2002::1, false for a public control (93.184.216.34).

bin/rails test test/models/ssrf_protection_test.rb → 19 tests, 0 failures.

DISALLOWED_IP_RANGES covered the NAT64 well-known prefix (RFC6052,
64:ff9b::/96) and 6to4 (RFC3056, 2002::/16) — both encapsulate IPv4
destinations and can be used to smuggle otherwise-blocked targets
past SSRF filtering. Add both ranges and cover them with
blocked_address? assertions plus a public control.

Weekend run #3873122.
Copilot AI review requested due to automatic review settings July 20, 2026 09:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens SsrfProtection against SSRF bypasses via IPv6 encapsulation schemes by expanding the disallowed IP ranges to include NAT64 and 6to4 prefixes, with tests covering the newly blocked address forms.

Changes:

  • Add NAT64 well-known prefix (64:ff9b::/96) and 6to4 (2002::/16) to SsrfProtection::DISALLOWED_IP_RANGES.
  • Add unit tests asserting these encapsulated IPv6 addresses are blocked (and a public control is allowed) via blocked_address?.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
app/models/ssrf_protection.rb Extends the SSRF denylist to cover NAT64 and 6to4 IPv6 encapsulation ranges.
test/models/ssrf_protection_test.rb Adds coverage to ensure the new encapsulation ranges are blocked (plus a public allow control).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

IPAddr.new("198.18.0.0/15") # Benchmark testing (RFC2544)
IPAddr.new("198.18.0.0/15"), # Benchmark testing (RFC2544)
IPAddr.new("64:ff9b::/96"), # NAT64 well-known prefix (RFC6052)
IPAddr.new("2002::/16") # 6to4 (RFC3056)
@jeremy

jeremy commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

🤖 Closing in deference to #2988 (security/ssrf-ipv6-transition-guard), which lands a more complete IPv6 SSRF guard: it extracts the embedded IPv4 from a NAT64 address and re-checks it (so NAT64→public still resolves while NAT64→internal is blocked), blocks 6to4/Teredo outright, and adds comprehensive IPv4/IPv6 special-use ranges. That supersedes this range-blocklist approach. Thanks @djmb.

One gap to carry over — noted on #2988 directly: the well-known-prefix check keys only on 64:ff9b::/96, so the RFC 8215 local-use NAT64 prefix 64:ff9b:1::/48 (e.g. 64:ff9b:1::a00:110.0.0.1) still reads as public.

@jeremy jeremy closed this Jul 20, 2026
@djmb

djmb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Both follow-ups from this PR are now handled on #2988 (95b2d43): the RFC 8215 local-use NAT64 prefix 64:ff9b:1::/48 goes through the same embedded-IPv4 recheck as the well-known prefix, and 2001:2::/48 (IPv6 benchmarking) is blocked to match 198.18.0.0/15.

On the Copilot trailing-comma nit: the cited constants (Webhook::PERMITTED_ACTIONS, ActivitiesController::ACTIONS) are %w[] arrays with no commas at all, and rubocop-rails-omakase doesn't require trailing commas — so no style change needed.

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.

3 participants