How loose redirect_uri regex validation, path traversal, and missing state parameters allow attackers to steal authorization codes and hijack user accounts.
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.
The OAuth provider allows wildcard redirects like https://app.com/* or loose subdomain matching.
The attacker creates an authorization link: provider.com/auth?client_id=123&redirect_uri=https://app.com/logout?redirect=https://evil.com.
The victim clicks the link and authenticates. The authorization server redirects to the victim app's open redirect.
The open redirect forwards the victim's browser to evil.com?code=SECRET_AUTH_CODE, allowing the attacker to claim the account.
# 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")
# 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")
redirect_uri (never allow wildcards, regex, or open subdomains).state parameter on every authorization flow.Referer header.