Skip to content

harden: fix SSRF guard bypass via IPv6 transition addresses - #902

Merged
l5yth merged 1 commit into
mainfrom
l5y-web-ssrf-transition-addresses
Sep 1, 2026
Merged

l5yth merged 1 commit into
mainfrom
l5y-web-ssrf-transition-addresses

Conversation

@l5yth

@l5yth l5yth commented Sep 1, 2026

Copy link
Copy Markdown
Owner

restricted_ip_address? decided what federation may connect to by delegating to Ruby's IPAddr#loopback? / #private? / #link_local?. Those predicates classify an address by its literal form against a fixed table of special-use ranges; they do not decode IPv6 transition addressing, in which an IPv4 destination is embedded in an IPv6 address and reached through a gateway. A peer publishing an AAAA record of 64:ff9b::a9fe:a9fe passed the guard and build_remote_http_client pinned Net::HTTP#ipaddr to it, reaching 169.254.169.254 on any network with a NAT64 gateway. CWE-918, full-read SSRF against cloud instance metadata. #private? is also RFC1918-only, so CGNAT passed likewise.

The guard now matches an explicit RESTRICTED_IP_RANGES list and decodes transition encodings, re-checking the embedded IPv4 against that same list: NAT64 (64:ff9b::/96), 6to4 (2002::/16), Teredo (2001::/32, both the server IPv4 and the XOR-obfuscated client), and ISATAP (00-00-5E-FE / 02-00-5E-FE at IPv4 and the XOR-obfuscated client), and ISATAP (00-00-5E-FE / 02-00-5E-FE at bits 64..95, read independently of the prefix). Decoding rather than blanket-blocking is load-bearing: on an IPv6-only network with DNS64 every IPv4-only peer is synthesised into 64:ff9b::/96, so refusing the prefix would end federation with all of them while blocking nothing an attacker could use. Prefixes that are local or reserved by definition are restricted outright: 64:ff9b:1::/48 (RFC 8215) and ::/16, which subsumes ::1, IPv4-mapped and the deprecated IPv4-compatible form. Also newly restricted: 100.64.0.0/10 (RFC 6598), 0.0.0.0/8 (RFC 1122), fec0::/10 (RFC 3879).

The list is deliberately not a superset of the predicates it replaces. Ruby's #private? / #loopback? / #link_local? recognise an IPv4-mapped address by testing bits 80..95 == ffff without requiring bits 0..79 to be zero, so they misreport ordinary global addresses as internal; retiring that stdlib defect necessarily permits addresses they restricted. Two differential sweeps confirm every newly-permitted address falls in exactly that class and none embeds an internal target.

HT @tonghuaroot

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@l5yth
l5yth merged commit deaf89b into main Sep 1, 2026
11 checks passed
@l5yth
l5yth deleted the l5y-web-ssrf-transition-addresses branch September 1, 2026 08:26
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