fix(provider): backport "from" redirect validator to v1 (sibling of #275)#282
Conversation
Coverage Report for CI Build 25587333503Coverage increased (+0.03%) to 84.904%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
99b271c to
1ed7fdd
Compare
umputun
left a comment
There was a problem hiding this comment.
backport itself is byte-faithful to v2. provider/redirect.go, provider/redirect_test.go, and the token.AllowedHosts/AllowedHostsFunc addition all match v2 modulo the import path. Wiring in auth.go mirrors v2 line-for-line. Tests + race + lint clean in both modules.
three findings worth addressing, the first one is a real bug that lives in both v1 (this PR) and v2 (#275 as merged):
-
verify flow: the redirect validator can't fire in production.
sendConfirmationatprovider/verify.go:170-187buildsHandshake{State: "", ID: user + "::" + address}and never copiesr.URL.Query().Get("from"). The validator block atprovider/verify.go:151only seesconfClaims.Handshake.From, which is empty for every real login URL. The integration test passes because it directly mints a confirmation token withHandshake.Fromset, but production code can't produce such a token (the JWT is signed). Verified the same gap exists inv2/provider/verify.go:170-187, so it's not v1 drift, it's a bug in #275 that this PR faithfully reproduces. The README claims?from=<url>on/auth/<name>/loginis subject to the allowlist, but production never honorsfromfor verify in the first place. Two-part fix:- in both v1 and v2, add
From: r.URL.Query().Get("from")to the Handshake literal insendConfirmation. - replace the synthetic-token verify integration test with one that drives the public
/auth/<name>/login?from=...path end-to-end, so this kind of regression can't recur.
Since the underlying bug is in v2 too, this might warrant a separate issue/PR rather than blocking #282 on it. Up to you whether to land this PR as-is and file a follow-up, or fold the fix into both modules here.
- in both v1 and v2, add
-
missing oauth1 + apple rejection integration tests. The v2 module ships
TestOauth1LoginFromRejectsExternalHostandTestAppleHandler_LoginHandlerFromRejectsExternalHostas direct regression tests for the new gates atprovider/oauth1.go:166andprovider/apple.go:396. The v1 PR omits both. The "shared code path through embeddedParams" rationale in the description is true at the unit level (TestIsAllowedRedirectcovers the validator), but the rejection-branch wiring at the handler boundary is uncovered for these two providers, which is exactly what coveralls is flagging. About 30 lines each, copy-paste-ready from v2. -
untested bypass categories (also missing from v2, pre-existing gap, low priority): scheme-relative URLs (
//evil.com), userinfo bypass (https://allowed@evil.com), IPv6 ([::1]), IDN/punycode, percent-encoded hostnames, backslash variants. Verified manually that each is correctly rejected byurl.Parse+Hostname()semantics, so the validator is right, just not pinned. Worth a tracking issue for both modules.
note: coveralls' uncovered line at token/jwt.go:443 (AllowedHostsFunc.Get) is a false positive. The body is exercised through every test that constructs an AllowedHostsFunc, but coveralls' per-package report doesn't see cross-package execution. The v2 module has the same false positive and was merged. Not blocking.
40cb002 to
dc49fa8
Compare
|
addressed all three findings in the last push:
go fix + race + lint clean in both modules. |
) The "from" query parameter accepted by oauth1/oauth2/apple/verify login handlers was stored verbatim in the handshake JWT and used as the redirect target after a successful auth handshake with no validation. Any external URL passed as "from" became a 307 redirect after the user completed the real OAuth flow with the legitimate provider — usable for phishing and post-auth landing-page substitution. This is the same vulnerability fixed in v2 by #275; v1 was untouched. This PR ports the validator to v1 with the same opt-in policy: * token.AllowedHosts (interface) + AllowedHostsFunc (adapter), mirroring the existing token.Audience pattern. * Opts.AllowedRedirectHosts threaded through provider.Params, AppleHandler (via embedded Params) and VerifyHandler (own URL + AllowedRedirectHosts fields). * provider.isAllowedRedirect centralises the check; all four redirect call sites (oauth1.go:165, oauth2.go:241, apple.go:395, verify.go:141) gate on it and fall back to the existing JSON user-info response on rejection (with a [WARN] log via redirectHostForLog so attacker- supplied paths/queries do not leak into logs). Default (nil allowlist) is permissive — preserves pre-feature behaviour so existing consumers see no change. Hardening is enabled by setting Opts.AllowedRedirectHosts; passing an AllowedHostsFunc that returns nil restricts redirects to the service URL host only. Hostname comparison is case-insensitive and ignores the default port; non-http(s) schemes (javascript:, data:, ftp:) are rejected. Tests: * TestIsAllowedRedirect — 24 table cases covering permissive default, typed-nil guard, port equivalence, case-insensitivity, scheme rejection, allowlist matching. * TestRedirectHostForLog — 5 cases. * TestOauth2LoginFromRejectsExternalHost / TestOauth2LoginFromAllowsAllowlistedHost — integration coverage of the oauth2 path (negative + positive). * TestVerifyHandler_LoginAcceptConfirmFromRejectsExternalHost / TestVerifyHandler_LoginAcceptConfirmFromAllowsAllowlistedHost — integration coverage of the verify path (negative + positive). oauth1 and apple share the same code path through the embedded Params; the unit tests and shared isAllowedRedirect cover them.
dc49fa8 to
2c9a686
Compare
|
folded the v1 + v2 URL bypass-category characterization tests into
pure characterisation -- no behaviour change. v2 tests added too even though v2 redirect.go is already merged in #275, so the bypass coverage stays in lockstep across both modules. |
umputun
left a comment
There was a problem hiding this comment.
all 3 round-1 findings addressed cleanly:
sendConfirmationHandshake.From bug fixed in both v1 and v2; newTestVerifyHandler_PublicFromFlow_RejectsExternalHostdrives the public flow end-to-end with the regression-pin assertion onparsed.Handshake.From.- v1 oauth1 + apple rejection integration tests added, mirroring v2.
- bypass-category characterization tests folded in even though I marked them low-priority deferrable. Nice. Coveralls now SUCCESS at 84.9% (+0.03%).
LGTM, thx.
Summary
The
"from"query parameter accepted byoauth1/oauth2/apple/verifylogin handlers was stored verbatim in the handshake JWT and used as the redirect target after a successful auth handshake with no validation. Any external URL passed asfrombecame a 307 redirect after the user completed the real OAuth flow with the legitimate provider — usable for phishing and post-auth landing-page substitution.This is the same vulnerability fixed in v2 by #275. v1 was untouched. This PR ports the validator with the same opt-in policy.
Changes
token.AllowedHosts(interface) +AllowedHostsFunc(adapter), mirroring the existingtoken.Audiencepattern.Opts.AllowedRedirectHoststhreaded throughprovider.Params,AppleHandler(via embeddedParams) andVerifyHandler(ownURL+AllowedRedirectHostsfields).provider.isAllowedRedirectcentralises the check; all four redirect call sites gate on it and fall back to the existing JSON user-info response on rejection, logging viaredirectHostForLogso attacker-supplied paths/queries do not leak into logs:oauth1.go:165→AuthHandleroauth2.go:241→AuthHandlerapple.go:395→LoginHandlerverify.go:141→LoginHandlerPolicy
Default (
nilallowlist) is permissive — preserves pre-feature behaviour so existing consumers see no change. Hardening is enabled by settingOpts.AllowedRedirectHosts. Passing anAllowedHostsFuncthat returnsnilrestricts redirects to the service URL host only.https://xandhttps://x:443match)javascript:,data:,ftp:)AllowedHostsFuncguarded (treated as no allowlist configured)Tests
TestIsAllowedRedirect— 24 table cases covering permissive default, typed-nil guard, port equivalence, case-insensitivity, scheme rejection, allowlist matching, getter error handling.TestRedirectHostForLog— 5 cases.TestOauth2LoginFromRejectsExternalHost/TestOauth2LoginFromAllowsAllowlistedHost— integration coverage of the oauth2 path (negative + positive).TestVerifyHandler_LoginAcceptConfirmFromRejectsExternalHost/TestVerifyHandler_LoginAcceptConfirmFromAllowsAllowlistedHost— integration coverage of the verify path (negative + positive).oauth1andappleshare the same code path through the embeddedParams; the unit tests and sharedisAllowedRedirectcover them.Full
go test -race ./...green;golangci-lint run --new-from-rev=master0 issues.Credit
Thanks to Admir Bajric (@AdmirBajric) for flagging on 2026-05-08 that v1 was still vulnerable after #275 fixed v2 — that report prompted this backport.