fix(auth/oauth2): block SSRF via IPv4-mapped IPv6 and fix dead 172.16/12 check#341
Closed
g0w6y wants to merge 1 commit into
Closed
fix(auth/oauth2): block SSRF via IPv4-mapped IPv6 and fix dead 172.16/12 check#341g0w6y wants to merge 1 commit into
g0w6y wants to merge 1 commit into
Conversation
kalenkevich
reviewed
May 13, 2026
Collaborator
kalenkevich
left a comment
There was a problem hiding this comment.
Thanks a a lot, can you please provide some tests as well?
…/12 check
Three bugs fixed in validateDiscoveryUrl() — both call sites
(discoverAuthServerMetadata and discoverResourceMetadata) pass
user-controlled URLs directly into this function.
Bug 1 — SSRF via IPv4-mapped IPv6 (CWE-918)
The WHATWG URL parser returns IPv4-mapped addresses in compressed hex
e.g. [::ffff:7f00:1], not 127.0.0.1. Nothing in the old blocklist
matched that form, bypassing every blocked IPv4 range:
https://[::ffff:7f00:1]/ -> 127.0.0.1 (loopback)
https://[::ffff:a9fe:a9fe]/ -> 169.254.169.254 (IMDS)
https://[::ffff:a00:1]/ -> 10.0.0.1 (RFC 1918)
https://[::ffff:ac10:1]/ -> 172.16.0.1 (RFC 1918)
https://[::ffff:c0a8:101]/ -> 192.168.1.1 (RFC 1918)
Fix: normaliseHostname() unwraps [::ffff:HHHH:LLLL] to dotted-decimal
before any blocklist check runs.
Bug 2 — Dead 172.16.0.0/12 check
Variable declared as secondOcte, condition referenced secondOctet.
ReferenceError silently caught — entire 172.16-172.31 block unreachable.
Fix: rename secondOcte to secondOctet.
Bug 3 — IPv6 ULA fd::/8 bypass (regex missing 'd')
/^\[f[ce89ab][0-9a-f]{2}:/ blocked fc::/8 but not fd::/8.
fd::/8 is Locally Assigned ULA (RFC 4193 3.2.2) — the default prefix
in Docker, Kubernetes and AWS dual-stack environments.
https://[fd00::1]/ -> validateDiscoveryUrl returned true (SSRF)
https://[fdab:1234::1]/ -> validateDiscoveryUrl returned true (SSRF)
Fix: /^\[(f[cd][0-9a-f]{2}|fe[89ab][0-9a-f]):/i
f[cd] covers fc::/8 and fd::/8 (full fc00::/7)
fe[89ab] covers fe80-febf (precise fe80::/10)
Also adds previously missing blocked ranges:
0.0.0.0 — unspecified, routes to loopback on many stacks
100.64.0.0/10 — RFC 6598 CGNAT shared address space
fc00::/7 — IPv6 ULA (both halves)
fe80::/10 — IPv6 link-local
Tests: 27 cases added to oauth2_discovery_test.ts covering all blocked
ranges, both edges of each range, the five IPv4-mapped bypass vectors,
the fd::/8 regression, and legitimate public IdP URLs.
5b488ea to
4417c96
Compare
Contributor
Author
|
Tests added in 4417c96 covering all fix vectors please review @kalenkevich |
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.
validateDiscoveryUrl()had three bugs that allowed SSRF via crafted URLs. BothdiscoverAuthServerMetadata()anddiscoverResourceMetadata()pass user-controlled URLs directly into this function.What was broken
1. IPv4-mapped IPv6 bypass (CWE-918)
The WHATWG URL parser serialises IPv4-mapped addresses as compressed hex
[::ffff:7f00:1]not127.0.0.1. The blocklist never matched, so every blocked IPv4 range was reachable via its IPv6-mapped form.2. Dead 172.16.0.0/12 check
secondOctewas declared butsecondOctetwas referenced in the condition. ReferenceError silently caught the entire 172.16–172.31 range was never blocked.3. IPv6 ULA fd::/8 not blocked
The regex
f[ce89ab]matchedfc::/8but missedfd::/8. RFC 4193 §3.2.2 definesfd::/8as Locally Assigned ULA the prefix used by default in Docker, Kubernetes, and AWS dual-stack.What was fixed
normaliseHostname()to unwrap[::ffff:HHHH:LLLL]to dotted-decimal before any blocklist checksecondOcte→secondOctet/^\[(f[cd][0-9a-f]{2}|fe[89ab][0-9a-f]):/icovers bothfc::/8andfd::/80.0.0.0,100.64.0.0/10,fc00::/7,fe80::/10Tests added