Fix guide · high · oauth_redirect_uri_dynamic
OAuth redirect_uri built from user input or template
Your OAuth authorize URL builds redirect_uri dynamically — using template-literal interpolation, window.location, or query-param substitution. If the OAuth provider's allowlist is permissive, an attacker can redirect the auth code to a domain they control.
Why it matters
OAuth security depends on the provider's redirect-URI allowlist being *exact-match* and *fully-enumerated*. The contract: at registration time, you tell the provider every URL you'll ever use as a callback; at flow time, the provider only redirects to one of those URLs.
When your client builds redirect_uri dynamically, two things go wrong together:
- The set of redirect URIs your app actually uses becomes unbounded — if it's
${window.location.origin}/callback, every preview deploy URL, every staging branch, every reverse-proxy hostname becomes a valid redirect. - Operators respond by relaxing the provider's allowlist — they add wildcards, or they add a URL with permissive subpath matching, or they add a long list including a domain they no longer control. Whatever they do, the allowlist is no longer the airtight check it was designed to be.
Combine these and you have a *covert auth-code redirection*: an attacker constructs a flow that exits to their domain, the provider permits the redirect because of the loosened allowlist, the attacker's site receives the auth code, exchanges it (if they have the client_secret — which is why oauth_client_secret_in_client is a critical companion) or just uses the code-shaped data for a known-flow attack.
This is the underlying mechanism of most public OAuth-related disclosures (notably the 2014 Facebook open-redirect-as-OAuth-callback chain, and the 2023 round of Microsoft/Azure tenant-confused-deputy CVEs).
How to fix it
- Hard-code the redirect_uri in your bundle. It should be a single fixed URL string, not a template.
``js // BAD const redirectUri = ${window.location.origin}/auth/callback; // GOOD const redirectUri = "https://app.example.com/auth/callback"; ``
- Ship a different bundle per environment. Production gets the production callback URL baked in; staging gets the staging URL. Don't try to one-size-fits-all the bundle.
- At the OAuth provider, list ONLY the exact production callback URL. Remove every wildcard, every subdomain entry you don't actively need, and every URL pointing at a domain you no longer control.
- For multi-tenant apps, redirect to a single dispatcher route (
/auth/callback) and resolve the tenant from thestateparameter or a server-side session, not fromredirect_uri. - PKCE doesn't fix this. PKCE binds the code-exchange to the original flow, which mitigates code-interception but does NOT prevent the code from being delivered to a permissively-allowlisted callback in the first place.
Did vibecheck flag this on your app?
If you reached this page from a vibecheck inspection report, the redacted match in your scan output is the exact string we found in your bundle. After applying the fix above, run the inspection again — the finding should clear.
Run another inspection