fix(oauth): validate redirect_uri in POST /api/authorize and /callback error path - #266
Open
andesyteoss wants to merge 1 commit into
Open
fix(oauth): validate redirect_uri in POST /api/authorize and /callback error path#266andesyteoss wants to merge 1 commit into
andesyteoss wants to merge 1 commit into
Conversation
…e (CWE-601) The /callback route decoded the OAuth state and used the embedded redirectUri as the destination of its 307 (both on success and when relaying upstream errors) without re-validating that the URI was on the client's registered redirect_uris allowlist. POST /api/authorize re-encoded a caller-supplied state into a fresh upstream auth URL without re-validating clientId/redirectUri, so an attacker could supply a state with an attacker-controlled redirectUri and the authorization code would be redirected there once the upstream IdP round-tripped it back to /callback. Both paths now use matchesRedirectUri() (the same RFC 8252 loopback matcher used by GET /api/authorize) before any redirect is emitted. Adds an integration test that asserts the /callback route refuses to redirect to an off-allowlist URI on both success and error paths.
andesyteoss
requested review from
Shridhad,
andrelandgraf and
pffigueiredo
as code owners
May 15, 2026 02:52
|
@sebastiondev is attempting to deploy a commit to the neondatabase Team on Vercel. A member of the Team first needs to authorize it. |
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.
Vulnerability: Open Redirect via crafted OAuth state (CWE-601)
Severity: High
Affected files:
landing/app/api/authorize/route.ts,landing/app/callback/route.tsSummary
The OAuth authorization flow is vulnerable to an open redirect. An attacker can craft a base64-encoded
stateparameter containing an arbitraryredirectUriand POST it directly to/api/authorize. This state is forwarded to the upstream identity provider, which round-trips it back to/callback. The callback handler then redirects the authorization code to whateverredirectUriwas embedded in the state — including attacker-controlled domains.This allows an attacker to steal OAuth authorization codes by tricking a user into completing the OAuth flow through a link that ultimately redirects the code to a server the attacker controls.
Data flow
btoa(JSON.stringify({ clientId: "legit-client", redirectUri: "https://attacker.example/steal", ... }))/api/authorizewith this state. The POST handler decodes the state but does not validateredirectUriagainst the client's registered redirect URIs — it re-encodes it and forwards it to the upstream IdP./callback?code=...&state=.../callbackdecodes the state again and issues a 307 redirect toredirectUriwith the authorization code appended.https://attacker.example/stealreceives the code.The GET handler for
/api/authorizedoes validate the redirect URI when it initially builds the state, but the POST handler accepts externally supplied state without re-validation, bypassing that check entirely.A second path exists in the error-relay branch of
/callback: when the upstream IdP returns an error, the callback decodes the state and redirects the error toredirectUri— also without validation.Proof of concept
Fix description
POST
/api/authorize: Before re-encoding the state for the upstream IdP, the handler now loads the client byclientIdfrom the decoded state and validatesredirectUriagainst the client's registeredredirect_urisusing the existingmatchesRedirectUrihelper. Requests with unknown clients or non-allowlisted redirect URIs are rejected with a 400 error.GET
/callback(error relay path): A newisAllowedRedirectUrihelper validates the decodedredirectUriagainst the client's registered URIs before relaying upstream errors. On validation failure or database errors, the handler fails closed — it returns a 400 instead of redirecting.Both paths use the same
matchesRedirectUrifunction fromlib/oauth/redirect-uri.tsthat already powers the GET/api/authorizehandler, so the validation logic is consistent across all entry points.Testing
Added integration tests in
mcp-src/__tests__/auth-callback-open-redirect.integration.test.tscovering:redirectUriis not on the client's allowlist.redirectUristill produces a 307 with the authorization code.Adversarial review
Before submitting, we considered whether existing protections prevent exploitation. The GET
/api/authorizehandler does validate redirect URIs when building the initial state — but this is irrelevant because the POST handler accepts arbitrary externally-crafted state directly, completely bypassing the GET handler. There are no CSRF protections on the POST endpoint that would prevent an attacker from calling it directly. The upstream IdP treats the state as opaque and passes it through unchanged, so it cannot act as a guardrail either.