Back to Blog
critical SEVERITY7 min read

How OAuth 2.0 CSRF happens in PHP and how to fix it

A critical OAuth 2.0 CSRF vulnerability in `login_weibo.php` allowed attackers to forge Weibo login requests by exploiting the missing `state` parameter validation. Without this check, an attacker could trick a victim's browser into completing an OAuth flow with the attacker's authorization code, potentially hijacking the victim's session. The fix generates a cryptographically random state token, stores it in the session, and validates it on callback.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This is an OAuth 2.0 CSRF vulnerability (CWE-352) in PHP's `login_weibo.php`, where the authorization callback accepted the `code` GET parameter without validating a `state` token, violating RFC 6749 §10.12. The fix generates a 16-byte cryptographically random state value using `bin2hex(random_bytes(16))`, stores it in `$_SESSION['oauth_weibo_state']`, appends it to the Weibo authorization URL, and verifies it on callback before processing the authorization code.

Vulnerability at a Glance

cweCWE-352
fixGenerate a random state token per authorization request, store in session, and validate on callback before exchanging the code
riskAttacker can force a victim to log in as the attacker, enabling account takeover or session fixation
languagePHP
root causeThe OAuth callback in login_weibo.php processed `$_GET['code']` without verifying a session-bound state token
vulnerabilityOAuth 2.0 CSRF (Missing State Parameter)

How OAuth 2.0 CSRF Happens in PHP and How to Fix It

The Incident

The login_weibo.php file in this Online Judge platform handles Weibo OAuth 2.0 login — a common social login flow where users authorize the app via Weibo's authorization server and are redirected back with an authorization code. But a critical flaw on line 22 meant that any code value arriving via a GET request would be blindly accepted and exchanged for an access token, with no verification that the request originated from a legitimate flow initiated by the actual user.

This is a textbook OAuth 2.0 CSRF vulnerability, and it's more dangerous than it sounds.


The Vulnerability Explained

What Was Missing

OAuth 2.0's RFC 6749 (§10.12) explicitly requires the use of a state parameter to prevent CSRF. The state value is:
1. Generated by the client before redirecting the user to the authorization server
2. Stored in the user's session
3. Sent along with the authorization request
4. Returned by the authorization server in the callback
5. Validated by the client before proceeding

In the vulnerable version of login_weibo.php, steps 1–5 were entirely absent. Here's the original callback handler:

// VULNERABLE CODE (before fix)
if (isset($_GET['code'])) {
    $code = $_GET['code'];
    $GURL = "https://api.weibo.com/oauth2/access_token?";
    $vars = array(
        // ... token exchange parameters
    );
    // Immediately exchanges code for token — no origin check!

And the authorization redirect:

// VULNERABLE CODE (before fix)
} else {
    $CBURL = "https://api.weibo.com/oauth2/authorize?client_id={$OJ_WEIBO_AKEY}&response_type=code&redirect_uri=$OJ_WEIBO_CBURL";
    header("Location: " . $CBURL);
}

Notice that no state parameter is generated or appended to the Weibo authorization URL, and the callback performs zero session-bound validation.

The Attack Scenario

Here's exactly how an attacker would exploit this against a user of this Online Judge platform:

  1. Attacker initiates their own OAuth flow against the target OJ instance, obtaining a valid Weibo authorization code (ATTACKER_CODE) linked to their Weibo account.

  2. Attacker crafts a malicious page containing a hidden request to the callback URL:
    html <img src="https://target-oj.com/login_weibo.php?code=ATTACKER_CODE" style="display:none">

  3. Victim visits the malicious page while logged out of the OJ (or with an active session). The victim's browser silently fires a GET request to login_weibo.php?code=ATTACKER_CODE.

  4. The server exchanges ATTACKER_CODE for an access token tied to the attacker's Weibo account and logs the victim in as the attacker.

  5. Result: The victim is now authenticated as the attacker. Any actions the victim takes — submitting solutions, viewing private data — are performed under the attacker's identity. Conversely, the attacker can monitor what the victim does if session state is shared.

This is a 2-step exploit chain: craft the malicious page, deliver it to the victim. No phishing of credentials required.

Real-World Impact for This Application

This is a competitive programming / Online Judge platform. Exploitation could allow:
- Account takeover by association: Force a victim to log in as the attacker, allowing the attacker to submit solutions under the victim's contest account
- Session fixation: Depending on session handling, the attacker may be able to predict or share session state
- Trust abuse: Any contest submissions, rankings, or private problem sets the victim accesses would be attributed to or visible by the attacker's account


The Fix

The fix implements the complete OAuth 2.0 state parameter flow in two places.

Step 1: Generate and Store State on Authorization

// FIXED: Authorization redirect
} else {
    $state = bin2hex(random_bytes(16));          // 32-char hex, 128 bits of entropy
    $_SESSION['oauth_weibo_state'] = $state;     // Bind to user's session
    $CBURL = "https://api.weibo.com/oauth2/authorize?client_id={$OJ_WEIBO_AKEY}&response_type=code&redirect_uri=$OJ_WEIBO_CBURL&state=" . urlencode($state);
    header("Location: " . $CBURL);
}

random_bytes(16) generates 16 cryptographically secure random bytes (128 bits of entropy), which bin2hex() encodes as a 32-character hex string. This value is stored in $_SESSION['oauth_weibo_state'] and appended to the Weibo authorization URL. Weibo will echo this value back in the callback.

Step 2: Validate State Before Processing the Code

// FIXED: Callback handler
if (isset($_GET['code'])) {
    if (!isset($_GET['state']) || 
        !isset($_SESSION['oauth_weibo_state']) || 
        $_GET['state'] !== $_SESSION['oauth_weibo_state']) {
        echo "Invalid state parameter!";
        exit;
    }
    unset($_SESSION['oauth_weibo_state']);   // Consume the token — prevents replay
    $code = $_GET['code'];
    // ... proceed with token exchange

The validation checks three conditions:
1. $_GET['state'] must be present in the callback
2. $_SESSION['oauth_weibo_state'] must exist (i.e., a legitimate flow was started)
3. The two values must match exactly (strict !== comparison)

After validation, unset($_SESSION['oauth_weibo_state']) removes the token so it cannot be replayed in a second request.

Before/After Comparison

Aspect Before After
State generated? ❌ No ✅ bin2hex(random_bytes(16))
State in auth URL? ❌ No ✅ &state=... appended
State stored in session? ❌ No ✅ $_SESSION['oauth_weibo_state']
Callback validates state? ❌ No ✅ Three-condition check
Token consumed after use? ❌ N/A ✅ unset($_SESSION['oauth_weibo_state'])

Key Takeaways

  • login_weibo.php accepted $_GET['code'] with zero origin verification — any page on the internet could trigger a Weibo login as any Weibo user
  • The state parameter in OAuth 2.0 is a CSRF token — treat it with the same rigor as form CSRF tokens: generate cryptographically, store server-side, validate strictly, consume after use
  • bin2hex(random_bytes(16)) is the correct PHP idiom for generating unpredictable tokens — never use rand(), mt_rand(), or uniqid() for security tokens
  • unset($_SESSION['oauth_weibo_state']) after validation prevents state token replay attacks, which could otherwise allow a second forged request to succeed
  • Social login callbacks are high-value targets — they sit at the boundary between an external identity provider and your application's session system, making them attractive for CSRF and session fixation attacks

How Orbis AppSec Detected This

  • Source: The code GET parameter ($_GET['code']) arriving at the OAuth callback URL — attacker-controlled input from any origin
  • Sink: Direct consumption of $_GET['code'] in the Weibo token exchange request at trunk/web/login_weibo.php:22, without any session-bound origin check
  • Missing control: No state parameter generated during authorization initiation, no session storage of expected state, and no comparison of $_GET['state'] against a stored value in the callback handler
  • CWE: CWE-352 — Cross-Site Request Forgery
  • Fix: Generated a 128-bit random state token with bin2hex(random_bytes(16)), stored it in $_SESSION['oauth_weibo_state'], appended it to the Weibo authorization URL, and added strict three-condition validation at the callback entry point with immediate token consumption via unset()

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The missing state parameter in login_weibo.php is a subtle but critical flaw — the OAuth flow worked correctly for legitimate users, making it easy to overlook in code review. Yet it left every user of this Online Judge platform vulnerable to a trivial 2-step CSRF attack requiring nothing more than a crafted image tag.

The fix is clean, minimal, and follows RFC 6749 exactly: generate a random token, bind it to the session, validate it on return. Two added blocks of code eliminate the entire attack surface without touching any other functionality.

OAuth 2.0 is widely implemented but frequently misimplemented. The state parameter is the single most commonly omitted security control in social login integrations. If you maintain any OAuth callback handler in PHP — or any language — verify today that you're generating, storing, and validating state on every authorization flow.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1184

Related Articles

critical

Music Studio App Express Server: CWE-287 Authentication Bypass via

A critical authentication bypass vulnerability in a music studio application's Express server allowed remote attackers to access API endpoints despite the intended localhost-only restriction. The localRequest middleware validated the Host header but failed to verify the actual remote socket address, enabling trivial bypasses in containerized and multi-user environments.

high

CVE-2026-104850: MCP TypeScript SDK OAuth Credential Leak

The MCP TypeScript SDK (`@modelcontextprotocol/sdk`) contained a flaw in its OAuth client flow where the MCP server being connected to could influence which authorization server the client talked to, meaning client credentials and token-exchange traffic could be directed to an endpoint the attacker controls. This project was pinned to the affected `1.30.0`; the dependency has been moved to `^1.31.0`, which carries the upstream fix for CVE-2026-104850. Any MCP client that performs OAuth against t

high

SQLite Auth Database Plaintext Storage in InitAuthDB()

The `InitAuthDB()` function opened SQLite databases without restricting filesystem permissions, leaving bcrypt password hashes, session tokens, and user settings exposed to any local account with read access. The fix explicitly sets `os.Chmod(filepath, 0600)` immediately after database creation, ensuring only the owner can access authentication secrets.

high

undici 8.10.0 Cache Poisoning: CVE-2026-85152 Auth Bypass

undici, the HTTP client used by Node.js's `fetch()` implementation, shipped a caching layer that did not properly isolate cached responses by origin, letting a response poisoned on one origin be served to requests for another. This created a path to cross-origin authentication bypass, tracked as CVE-2026-85152 and fixed in undici 8.10.2.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.