Skip to content

Block IPv6 addresses that reach internal IPs in the SSRF guard#2988

Merged
djmb merged 2 commits into
mainfrom
security/ssrf-ipv6-transition-guard
Jul 20, 2026
Merged

Block IPv6 addresses that reach internal IPs in the SSRF guard#2988
djmb merged 2 commits into
mainfrom
security/ssrf-ipv6-transition-guard

Conversation

@djmb

@djmb djmb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The SSRF guard blocked private, loopback, and link-local ranges (and the
IPv4-mapped/-compatible IPv6 forms) and CGNAT, but let through NAT64, 6to4,
and Teredo addresses, which can carry an internal IPv4.

This pulls the IPv4 out of a NAT64 address and checks that (so NAT64 to a
public site still works), blocks 6to4 and Teredo outright, and adds the
missing IPv4 and IPv6 special-use ranges.

The guard blocked the usual private, loopback, and link-local ranges (and
the IPv4-mapped/-compatible IPv6 forms) and CGNAT, but let through NAT64,
6to4, and Teredo addresses, which can point at an internal IPv4.

Now it pulls the IPv4 out of a NAT64 address and checks that (so NAT64 to a
public site still works), blocks 6to4 and Teredo outright, and adds the
missing IPv4 and IPv6 ranges.
Copilot AI review requested due to automatic review settings July 20, 2026 15:26

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 strengthens SsrfProtection by expanding the set of blocked IP ranges and closing IPv6 transition-mechanism gaps (NAT64/6to4/Teredo) that can be used to reach internal IPv4 targets via IPv6 addresses.

Changes:

  • Extracts and re-checks embedded IPv4s from well-known NAT64 IPv6 addresses.
  • Blocks 6to4 and Teredo IPv6 ranges outright and expands IPv4/IPv6 special-use range blocking.
  • Adds test coverage for NAT64, 6to4, Teredo, and additional IPv6 special-use cases.

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 Expands blocked special-use ranges and adds NAT64/6to4/Teredo-aware blocking logic.
test/models/ssrf_protection_test.rb Adds regression tests covering NAT64 embedded IPv4 checks and additional IPv6 transition/special-use cases.

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

Comment thread app/models/ssrf_protection.rb Outdated
Comment on lines +46 to +50
# Well-known NAT64 prefix (RFC 6052/6146). An address here embeds an IPv4
# target in its low 32 bits; extract it and re-check against the IPv4 rules so
# NAT64 to a public address still resolves while NAT64 to an internal address
# is blocked.
NAT64_WELL_KNOWN = IPAddr.new("64:ff9b::/96")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 95b2d43NAT64_PREFIXES now covers the RFC 8215 local-use prefix 64:ff9b:1::/48 alongside the well-known prefix, with the same embedded-IPv4 recheck and regression tests for both the private-embed (blocked) and public-embed (allowed) cases.

@jeremy

jeremy commented Jul 20, 2026

Copy link
Copy Markdown
Member

🤖 One gap worth closing while you're in here: NAT64_WELL_KNOWN keys only on 64:ff9b::/96 (RFC 6052). RFC 8215 also defines a local-use NAT64 prefix, 64:ff9b:1::/48, which embeds IPv4 the same way. So 64:ff9b:1::a00:1 decodes to 10.0.0.1 but falls through the WKP check into disallowed_ipv6?, where it reads as a public IPv6 address and is allowed — the embedded IPv4 never gets re-checked.

Suggest extending the well-known-prefix match (or the embedded_ipv4 recheck) to cover 64:ff9b:1::/48 as well. The low 32 bits carry the IPv4 in both prefixes, so the same embedded_ipv4(ipaddr) extraction works for local-use once the prefix guard admits it.

Repro address: 64:ff9b:1::a00:110.0.0.1 (should block); 64:ff9b:1::808:8088.8.8.8 (should still resolve).

The local-use NAT64 prefix (64:ff9b:1::/48, RFC 8215) embeds an IPv4
target in its low 32 bits just like the well-known prefix, so run it
through the same embedded-IPv4 recheck: local-use NAT64 to a public
address still resolves while local-use NAT64 to an internal address is
blocked.

Also block the IPv6 benchmarking range (2001:2::/48, RFC 5180) to match
the IPv4 benchmarking block on 198.18.0.0/15.
Copilot AI review requested due to automatic review settings July 20, 2026 16:08
@djmb

djmb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 95b2d43. NAT64_WELL_KNOWN is now NAT64_PREFIXES, covering both the well-known prefix and the RFC 8215 local-use prefix 64:ff9b:1::/48, with the same embedded-IPv4 recheck — 64:ff9b:1::a00:1 (→ 10.0.0.1) blocks, 64:ff9b:1::808:808 (→ 8.8.8.8) still resolves. Both cases have regression tests.

Also added 2001:2::/48 (IPv6 benchmarking, RFC 5180) to DISALLOWED_IPV6_RANGES to match the IPv4 198.18.0.0/15 block — the same gap Copilot caught on the once-campfire counterpart (basecamp/once-campfire#225).

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@djmb
djmb merged commit 62d3cad into main Jul 20, 2026
14 checks passed
@djmb
djmb deleted the security/ssrf-ipv6-transition-guard branch July 20, 2026 16:17
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