harden: fix SSRF guard bypass via IPv6 transition addresses - #902
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
restricted_ip_address?decided what federation may connect to by delegating to Ruby'sIPAddr#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 anAAAArecord of64:ff9b::a9fe:a9fepassed the guard andbuild_remote_http_clientpinnedNet::HTTP#ipaddrto it, reaching169.254.169.254on 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_RANGESlist 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-FEat IPv4 and the XOR-obfuscated client), and ISATAP (00-00-5E-FE/02-00-5E-FEat bits64..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 into64: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 bits80..95 == ffffwithout requiring bits0..79to be zero, so they misreport ordinary global addresses as internal; retiring thatstdlibdefect 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