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
Published August 26, 2026Reviewed 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

How Hardcoded Secrets Compromise Authentication in JavaScript and How to Fix It

A critical vulnerability in `Tool/QuantumultX/Rewrite/RRSP.js` exposed hardcoded API authentication credentials—a TOKEN and UMID device identifier—directly in source code. Anyone with repository access could extract these credentials to impersonate the legitimate user and gain full account access to the RRTV API service. The fix replaced hardcoded secrets with empty placeholders, forcing users to manually configure credentials through secure channels.

critical

How origin validation bypass happens in Express.js and how to fix it

A `POST /changeData` route in `src/main/server/routes/index.js` guarded state-changing writes with an origin allowlist, but the guard was wrapped in an `if (origin && ...)` truthiness check. Any request that simply omitted both `Origin` and `Referer` — a one-line `curl` command, a local script, a background process — skipped validation entirely and modified application data. The fix removes the truthiness short-circuit so a *missing* header is now treated as a rejection, not a pass.

critical

How CSRF vulnerabilities happen in Node.js API clients and how to fix them

A critical CSRF vulnerability in `lib/client.js` allowed attackers to forge authenticated POST requests to `/remote-ssh/api/*` endpoints. The fix adds the `X-Requested-With: XMLHttpRequest` header to enable proper CSRF token validation, blocking malicious cross-site requests with a minimal one-line change.

critical

How User Enumeration Happens in Django Forms and How to Fix It

A critical user enumeration vulnerability in the volunteers application allowed attackers to systematically discover registered email addresses through distinct error messages in signup and password reset forms. The fix replaces specific error messages with generic ones, preventing information disclosure while maintaining application functionality.

critical

How Authentication Bypass Happens in Node.js WebSocket Services and How to Fix It

The HousePanel push notification service exposed GET and POST endpoints without any authentication checks, allowing unauthenticated attackers to send arbitrary push notifications to connected smart devices. This critical vulnerability was fixed by implementing mandatory token validation on all protected endpoints, ensuring only authenticated requests can trigger push operations.

critical

How Missing API Authentication Happens in Node.js and How to Fix It

The GitHub API integration in `src/github.mjs` was making unauthenticated requests, subjecting the application to GitHub's strict 60 requests/hour rate limit. This fix adds secure authentication token injection from environment variables using conditional header spreading, enabling authenticated requests with a much higher rate limit (5,000 requests/hour).