Application Security
oauth
oauth2
authentication
sso
+3 more

OAuth 2.0 Vulnerabilities: Every Attack and Defense Explained with Code

SCRs Team
March 28, 2026
16 min read
Share

Why OAuth 2.0 Is Still Broken in 2026

OAuth 2.0 powers authentication for billions of users — Google, GitHub, Facebook, Microsoft — yet implementation errors remain one of the most common web vulnerabilities.

The problem isn't the spec. It's that OAuth has dozens of configuration options, and getting even one wrong opens a critical vulnerability.

VulnerabilityImpactPrevalence
Open Redirect in redirect_uriAccount hijackingVery common
CSRF (missing state parameter)Account linking attacksCommon
Authorization code interceptionFull account takeoverModerate
Token leakage via Referer headerSession hijackingCommon
PKCE bypassMobile app account takeoverIncreasing
Scope escalationPrivilege elevationModerate

Attack #1: Open Redirect via redirect_uri Manipulation

The most common OAuth vulnerability. If the OAuth provider doesn't strictly validate the redirect_uri, an attacker can steal authorization codes.

The Attack Flow

1. Attacker crafts malicious URL:
   https://oauth.provider.com/authorize?
     client_id=legit_app&
     redirect_uri=https://legit-app.com.evil.com/callback&
     response_type=code&
     state=random

2. User clicks link, authenticates with provider
3. Provider redirects to attacker's domain with the auth code:
   https://legit-app.com.evil.com/callback?code=AUTH_CODE_HERE

4. Attacker exchanges code for access token
5. Attacker now has access to user's account

Defense: Exact redirect_uri Matching

// ✅ Server-side: Validate redirect_uri EXACTLY
const ALLOWED_REDIRECTS = [
  'https://myapp.com/auth/callback',
  'https://staging.myapp.com/auth/callback',
];

function validateRedirectUri(uri: string): boolean {
  return ALLOWED_REDIRECTS.includes(uri);
  // ❌ Never use: uri.startsWith('https://myapp.com')
  // ❌ Never use: uri.includes('myapp.com')
  // ❌ Never use: regex matching on domain
}

Attack #2: CSRF — Missing State Parameter

Without a state parameter, an attacker can link their own OAuth account to a victim's session.

The Attack

1. Attacker starts OAuth flow, gets authorization code
2. Attacker crafts URL: 
   https://victim-app.com/callback?code=ATTACKERS_CODE
3. Victim visits the link (via phishing, embedded image, etc.)
4. Victim's account is now linked to attacker's Google account
5. Attacker can now log in to victim's account via "Login with Google"

Defense: Cryptographic State Parameter

import crypto from 'crypto';

// Generating the authorization URL
function buildAuthUrl(session: Session): string {
  // ✅ Generate cryptographic random state
  const state = crypto.randomBytes(32).toString('hex');
  
  // Store state in session (or signed cookie)
  session.oauthState = state;
  
  return \`https://oauth.provider.com/authorize?${new URLSearchParams({
    client_id: process.env.OAUTH_CLIENT_ID!,
    redirect_uri: 'https://myapp.com/auth/callback',
    response_type: 'code',
    state: state,
    scope: 'openid profile email',
  })}\`;
}

// Handling the callback
function handleCallback(req: Request, session: Session) {
  const { code, state } = req.query;
  
  // ✅ Verify state matches
  if (!state || state !== session.oauthState) {
    throw new Error('CSRF detected: state mismatch');
  }
  
  // Clear state after use
  delete session.oauthState;
  
  // Exchange code for token...
}

Attack #3: Authorization Code Interception (Mobile Apps)

On mobile devices, custom URL schemes (e.g., myapp://callback) can be registered by any app — including malicious ones.

Defense: PKCE (Proof Key for Code Exchange)

import crypto from 'crypto';

// Step 1: Generate code verifier and challenge
function generatePKCE() {
  const verifier = crypto.randomBytes(32)
    .toString('base64url');
  
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
  
  return { verifier, challenge };
}

// Step 2: Include challenge in authorization request
const { verifier, challenge } = generatePKCE();
// Store verifier securely

const authUrl = \`https://oauth.provider.com/authorize?${new URLSearchParams({
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  response_type: 'code',
  code_challenge: challenge,
  code_challenge_method: 'S256',
})}\`;

// Step 3: Include verifier in token exchange
const tokenResponse = await fetch('https://oauth.provider.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: verifier, // ✅ Proves we initiated the flow
  }),
});

PKCE is now required for ALL OAuth clients — not just mobile apps (OAuth 2.1 draft).


Attack #4: Token Leakage via Referer Header

If tokens are in the URL (implicit flow), they leak via the Referer header when users click any external link.

1. OAuth redirects to: https://myapp.com/callback#access_token=SECRET_TOKEN
2. Page loads, user clicks a link to https://external-blog.com
3. External site receives Referer header:
   Referer: https://myapp.com/callback#access_token=SECRET_TOKEN
4. External site now has the user's access token

Defense: Never Use Implicit Flow

❌ response_type=token    (Implicit flow — tokens in URL)
✅ response_type=code     (Authorization Code flow — codes in URL, tokens server-side)

Attack #5: Scope Escalation

1. App requests: scope=read_profile
2. Attacker modifies request: scope=read_profile admin delete_users
3. If provider doesn't validate against registered scopes,
   the token gets elevated permissions

Defense: Validate Scopes Server-Side

const REGISTERED_SCOPES = ['openid', 'profile', 'email'];

function validateScopes(requestedScopes: string[]): string[] {
  // ✅ Only allow scopes that are pre-registered
  return requestedScopes.filter(s => REGISTERED_SCOPES.includes(s));
}

Production OAuth Checklist

  • Exact redirect_uri matching (no wildcards, no regex)
  • Cryptographic state parameter on every request
  • PKCE enabled for all clients (public and confidential)
  • Authorization Code flow only (never Implicit)
  • Token storage in HttpOnly Secure cookies (not localStorage)
  • Short-lived access tokens (15 min) with refresh rotation
  • Scope validation against registered app permissions
  • Token revocation on logout
  • Rate limiting on token endpoint
  • Monitor for token reuse and code replay attacks

Advertisement