Skip to content

security(validators): stop lookalike-domain and IPv6-transition bypasses - #763

Merged
lakhansamani merged 1 commit into
mainfrom
security/allowlist-and-ssrf-bypasses
Aug 13, 2026
Merged

security(validators): stop lookalike-domain and IPv6-transition bypasses#763
lakhansamani merged 1 commit into
mainfrom
security/allowlist-and-ssrf-bypasses

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

Fixes 4 of the 6 advisories still open in triage. Verified live on main before fixing — these were not theoretical.

1. Allowlist bypass via unescaped regex dots — token theft

GHSA-89vc-q979-qwxv, GHSA-63w4-vxm7-v2mg (duplicates of each other), GHSA-j558-mp2m-fhrp (same root cause)

internal/validators/url.go spliced each configured origin into regexp.MatchString("^"+pattern+"$", ...). The wildcard branch escaped dots; the exact-origin branch — the one operators are told to use in production — did not. So https://login.acme.com compiled to ^login.acme.com$, in which every . matches any character.

Probed on main before the fix:

BYPASS: IsValidOrigin accepted https://loginxacme.com  against allowlist [https://login.acme.com]
BYPASS: IsValidOrigin accepted https://login-acme.com  against allowlist [https://login.acme.com]
BYPASS: IsValidRedirectURI accepted https://loginxacme.com ...

An attacker registers the lookalike and receives OAuth access tokens, ID tokens and password-reset tokens. This is an incomplete fix of the already-published GHSA-x3f4-v83f-7wp2 / CVE-2026-54072, which added these validators to stop exactly this class of leak.

Fix. Exact origins are compared as strings, never as patterns. Wildcards are regexp.QuoteMeta'd whole and then only the * is re-opened, so the wildcard is the single metacharacter surviving from operator config. Both callers now route through one originMatches helper — they previously carried copy-pasted blocks, which is precisely how one came to escape dots while the other did not.

Strictly a tightening: every legitimate origin that matched before still matches.

2. SSRF guard bypass via IPv6 transition addresses

GHSA-qxfq-3qvw-w5fr

isPrivateIP covered RFC 1918, CGNAT, loopback, link-local, ULA, multicast and reserved — but not the transition mechanisms that embed an arbitrary IPv4 address inside an IPv6 one. 2002:a9fe:a9fe:: (6to4) and 64:ff9b::a9fe:a9fe (NAT64) both resolve to 169.254.169.254, the cloud metadata endpoint, and sailed past every check.

Fix. Block 2002::/16 (6to4), 2001::/32 (Teredo), 64:ff9b::/96 (NAT64 well-known), 64:ff9b:1::/48 (RFC 8215 local-use), plus ::/128 and 100::/64. Blocking the ranges outright is stronger and simpler than decoding the embedded address, and costs nothing — none is a legitimate webhook or OIDC issuer destination.

Admin-privileged to reach (webhook endpoint / SSO OIDC discovery URLs), hence lower severity than #1.

Tests

internal/validators/allowlist_bypass_test.go19 assertions fail against main, confirmed by reverting both files and re-running.

  • Five lookalike shapes rejected for both validators, legitimate origin still accepted (including :443 normalisation)
  • Wildcard branch unchanged: proper subdomains match; bare domain, evil-acme.com, suffix-append and cross-label [^.]* all rejected
  • Regex metacharacters in config are literal (a+b.acme.com matches itself, not ab.acme.com)
  • Ten IPv6 transition addresses wrapping loopback / metadata / RFC 1918 all treated as private
  • Five ordinary public v4 and v6 addresses still allowed — the widened blocklist doesn't break real destinations

Verification

go build ./..., go vet ./..., make lint (0 issues), make test — all pass. The full suite matters here: tightening redirect validation could have broken callers, and did not.

Not included

GHSA-m82j-rq33-qjx2 (password-reset poisoning via Host header when --url is unset) is still open. It needs a change to default host-derivation behaviour that affects reverse-proxy and multi-tenant deployments — a deliberate decision, not a drive-by fix.

An exact allowlist entry was spliced into a regex with dots unescaped:
"https://login.acme.com" compiled to "^login.acme.com$", where every "."
matches any character. An attacker-registrable lookalike
(loginXacme.com, login-acme.com) passed IsValidRedirectURI and
IsValidOrigin, so OAuth access tokens, ID tokens and password-reset
tokens were delivered to their domain. Only the wildcard branch escaped
its dots — the exact branch is the documented production hardening.

Exact origins are now compared as strings, never as patterns. Wildcards
are QuoteMeta'd whole and only the "*" is re-opened. Both callers route
through one matcher; they previously carried copy-pasted blocks, which
is how one came to escape dots and the other did not.

Separately, the SSRF blocklist covered private IPv4 and IPv6 ranges but
not the transition mechanisms that embed an IPv4 address inside an IPv6
one: 2002:a9fe:a9fe:: (6to4) and 64:ff9b::a9fe:a9fe (NAT64) both reach
169.254.169.254. Block 2002::/16, 2001::/32, 64:ff9b::/96,
64:ff9b:1::/48, ::/128 and 100::/64 outright — none is a legitimate
webhook or OIDC destination.

Closes GHSA-89vc-q979-qwxv, GHSA-63w4-vxm7-v2mg, GHSA-j558-mp2m-fhrp,
GHSA-qxfq-3qvw-w5fr.
@lakhansamani
lakhansamani merged commit 59c4dd0 into main Aug 13, 2026
4 checks passed
@lakhansamani
lakhansamani deleted the security/allowlist-and-ssrf-bypasses branch August 13, 2026 13:31
@lakhansamani lakhansamani mentioned this pull request Aug 14, 2026
9 tasks
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