Back to Blog
critical SEVERITY8 min read

How session fixation happens in PHP OAuth login handlers and how to fix it

A session fixation vulnerability in `trunk/web/login_weibo.php` allowed attackers to hijack authenticated user sessions by pre-setting a victim's PHP session ID before they logged in via Weibo OAuth. The fix was a single, critical call to `session_regenerate_id(true)` inserted immediately before assigning the authenticated user's ID to the session. Left unpatched, this vulnerability could have given an attacker full access to any account whose Weibo OAuth login they could observe or influence.

O
By Orbis AppSec
Published July 29, 2026Reviewed July 29, 2026

Answer Summary

This is a session fixation vulnerability (CWE-384) in PHP's Weibo OAuth login handler (`login_weibo.php`). The root cause is that `$_SESSION['user_id']` was set after successful authentication without first calling `session_regenerate_id()`, meaning the pre-authentication session ID persisted into the authenticated session. An attacker who could plant a known session cookie on the victim (via subdomain injection or MITM) could then reuse that same session ID to access the authenticated account. The fix is a single line: call `session_regenerate_id(true)` before assigning any session variables after login succeeds.

Vulnerability at a Glance

cweCWE-384
fixAdded `session_regenerate_id(true)` immediately before the session assignment on line 68
riskAttacker hijacks an authenticated user session after Weibo OAuth login completes
languagePHP
root cause`$_SESSION[user_id]` assigned post-login without regenerating the session ID
vulnerabilitySession Fixation

How Session Fixation Happens in PHP OAuth Login Handlers and How to Fix It

Summary

A session fixation vulnerability in trunk/web/login_weibo.php allowed attackers to hijack authenticated user sessions by pre-setting a victim's PHP session ID before they logged in via Weibo OAuth. The fix was a single, critical call to session_regenerate_id(true) inserted immediately before assigning the authenticated user's ID to the session. Left unpatched, this vulnerability could have given an attacker full access to any account whose Weibo OAuth login they could observe or influence.


Introduction

The login_weibo.php file handles the OAuth callback flow for Weibo-based authentication — one of the most sensitive code paths in any web application. After exchanging the OAuth authorization code for an access token and resolving the user's identity, the handler writes the authenticated username into the PHP session:

$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
header("Location: ./");

This looks innocuous. But there is a critical missing step: the session ID is never rotated at the moment of privilege escalation. The same session token that existed before the user authenticated continues to exist after they authenticate — and that is the heart of a session fixation vulnerability.


The Vulnerability Explained

What is session fixation?

In a normal session hijacking attack, an adversary must steal a session token the server has already issued to a victim. Session fixation flips this: the attacker plants a known session token on the victim before they log in, then waits. Once the victim completes authentication, the server binds their authenticated identity to the attacker's pre-known token — and the attacker can immediately use that token to access the account.

The vulnerable code (before the fix)

Here is the exact code path in login_weibo.php around line 47–68:

// After OAuth token exchange and user lookup/creation:
pdo_query($sql, $uname, $email, $ip, $password, $nick, $school);

// login it
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;  // ← VULNERABLE: session ID unchanged
// redirect it
header("Location: ./");

The session variable $OJ_NAME . '_' . 'user_id' is set to $uname (the resolved Weibo username) without any call to session_regenerate_id(). PHP happily reuses the session file and cookie that was created when the unauthenticated user first visited the site.

Step-by-step attack scenario

This is a 3-step attack chain specific to this OAuth handler:

Step 1 — Session planting. The attacker visits the application to obtain a valid (unauthenticated) session ID, for example PHPSESSID=attacker_known_value. They then deliver this session cookie to the victim. Two practical delivery methods apply here:

  • Subdomain cookie injection: If the application runs at oj.example.com and any subdomain (e.g., static.example.com) can be influenced by the attacker, a Set-Cookie response from that subdomain with Domain=.example.com will plant the cookie in the victim's browser.
  • Network-level MITM: On a shared network (coffee shop Wi-Fi, corporate proxy), the attacker intercepts an HTTP response and injects a Set-Cookie header.

Step 2 — Victim authenticates. The victim clicks "Login with Weibo," completes the OAuth flow, and login_weibo.php runs. Because the victim's browser already holds PHPSESSID=attacker_known_value, PHP loads that session. The handler then executes:

$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;

The victim's Weibo username is now stored in the session file identified by attacker_known_value.

Step 3 — Session reuse. The attacker, who has always known attacker_known_value, sends a request with Cookie: PHPSESSID=attacker_known_value. The server reads the session, finds user_id = victim_username, and treats the attacker as the fully authenticated victim.

Real-world impact

  • Full account takeover of any user who completes Weibo OAuth login while the attacker's planted cookie is active.
  • The attacker does not need to know the victim's Weibo credentials.
  • The attack is silent — no failed login attempts, no brute-force signals.
  • In a containerized deployment (as noted in the threat model), if the application is network-exposed, this attack surface is reachable from outside the container network.

The Fix

The repair is minimal but decisive. A single line was inserted at line 68 of login_weibo.php, immediately before the session write:

Before

// login it
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
// redirect it
header("Location: ./");

After

// login it
session_regenerate_id(true);  // ← NEW: invalidate pre-auth session, issue fresh ID
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
// redirect it
header("Location: ./");

Why this specific change works

session_regenerate_id(true) does two things atomically:

  1. Issues a new session ID and sends a fresh Set-Cookie: PHPSESSID=<new_random_value> header to the client.
  2. Deletes the old session file on the server (the true argument). This is important: without true, the old session file remains on disk, and PHP may still accept the old ID depending on server configuration.

After this call, the attacker's pre-planted PHPSESSID=attacker_known_value is worthless. The server has deleted the corresponding session file, and the victim's browser now holds a brand-new session ID that the attacker does not know. The authenticated user_id is written only into this new session.

The placement matters: session_regenerate_id(true) must be called after the authentication decision is made (so we know the login succeeded) but before any privileged data is written to $_SESSION. This fix gets that ordering exactly right.


Prevention & Best Practices

Always regenerate session IDs at privilege escalation

The rule is simple: any time a session transitions from a lower-privilege state to a higher-privilege state, regenerate the session ID. This includes:

  • Successful username/password login
  • Successful OAuth callback (exactly this case)
  • Successful MFA verification
  • "Remember me" token redemption
  • Password reset completion
// Correct pattern for any PHP login handler
if (authenticate_user($username, $password)) {
    session_regenerate_id(true);           // Must come first
    $_SESSION['user_id'] = $username;      // Then write auth state
    header("Location: /dashboard");
}

Use true with session_regenerate_id()

Always pass true to delete the old session. Omitting this argument leaves an orphaned session file that could be reused in edge cases:

session_regenerate_id();       // ❌ Old session file persists
session_regenerate_id(true);   // ✅ Old session file deleted

Harden session cookies

Complement session regeneration with secure cookie attributes in php.ini or at runtime:

session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'domain'   => 'oj.example.com',  // Exact domain, not .example.com
    'secure'   => true,              // HTTPS only
    'httponly' => true,              // No JavaScript access
    'samesite' => 'Lax',            // CSRF mitigation
]);

Setting domain to the exact hostname (not a wildcard subdomain) also closes the subdomain cookie injection vector described in the attack scenario above.

Detect this pattern with static analysis

Semgrep can flag PHP files that write to $_SESSION after an authentication event without a preceding session_regenerate_id() call. A simple rule targets the exact pattern found here:

rules:
  - id: php-session-fixation
    patterns:
      - pattern: $_SESSION[$KEY] = $VAL;
      - pattern-not-inside: |
          session_regenerate_id(...);
          ...
          $_SESSION[$KEY] = $VAL;
    message: "$KEY written to session without prior session_regenerate_id()"
    languages: [php]
    severity: WARNING

Security standards

  • OWASP Session Management Cheat Sheet explicitly mandates session ID regeneration after login.
  • CWE-384: Session Fixation describes this exact weakness class.
  • ASVS v4.0, Requirement 3.3.1: "Verify that logout and expiration invalidate the session token, such that the back button or a downstream relying party does not resume an authenticated session."

Key Takeaways

  • session_regenerate_id(true) is mandatory in every PHP OAuth callback handler — not optional, not a "nice to have." login_weibo.php is a textbook example of what happens when it is omitted.
  • OAuth login handlers are high-value targets because they involve cross-domain redirects, making cookie planting via subdomain injection especially practical.
  • The true argument to session_regenerate_id() matters — without it, the old session file survives on disk and the protection is incomplete.
  • Placement is everything: regenerate the session ID after confirming authentication success but before writing $_SESSION[$OJ_NAME . '_' . 'user_id'].
  • Restricting the session cookie domain to the exact hostname (not a wildcard) removes the subdomain injection delivery vector that makes this attack practical in shared-domain deployments.

How Orbis AppSec Detected This

  • Source: The Weibo OAuth callback delivers an authenticated user identity ($uname) via the OAuth token exchange flow in login_weibo.php.
  • Sink: $_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname at line 68 of trunk/web/login_weibo.php — a privileged session write with no preceding session ID rotation.
  • Missing control: No call to session_regenerate_id() between the authentication decision and the session variable assignment.
  • CWE: CWE-384 — Session Fixation.
  • Fix: Inserted session_regenerate_id(true) on the line immediately preceding the $_SESSION assignment, ensuring the pre-authentication session ID is invalidated and deleted before any authenticated state is stored.

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

Session fixation is one of those vulnerabilities that is trivially easy to introduce and trivially easy to fix — but only if you know to look for it. In login_weibo.php, the entire attack surface was a single missing function call. An attacker exploiting this would have needed no knowledge of the victim's credentials, would have left no failed-login traces in logs, and could have achieved full account takeover silently.

The lesson for any developer writing PHP authentication code — whether for OAuth, SAML, password login, or MFA — is to treat session_regenerate_id(true) as a non-negotiable step in the login sequence, as automatic as calling session_start(). Pair it with tight cookie domain scoping, Secure and HttpOnly flags, and automated static analysis, and this entire class of vulnerability becomes effectively unreachable.


References

Frequently Asked Questions

What is session fixation?

Session fixation is an attack where an adversary sets or knows a victim's session ID before authentication, then reuses that same ID after the victim logs in to take over their authenticated session.

How do you prevent session fixation in PHP?

Always call `session_regenerate_id(true)` immediately after a successful login, before writing any authentication data to `$_SESSION`. The `true` argument deletes the old session file.

What CWE is session fixation?

Session fixation is classified as CWE-384: Session Fixation.

Is using HTTPS enough to prevent session fixation?

No. HTTPS prevents eavesdropping on session IDs in transit, but session fixation exploits the fact that the session ID does not change at privilege escalation. You must regenerate the session ID on login regardless of transport security.

Can static analysis detect session fixation?

Yes. Static analysis tools like Semgrep can flag patterns where `$_SESSION` is written after an authentication event without a preceding `session_regenerate_id()` call. Orbis AppSec's multi-agent AI scanner detected exactly this pattern in `login_weibo.php`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1183

Related Articles

critical

How Denial of Service via gzip bomb happens in Node.js tar and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) was discovered in the node-tar package where attackers could craft malicious gzip archives that expand to consume all available system resources. This vulnerability affected version 7.5.15 of the tar package and was fixed by upgrading to version 7.5.19. The fix protects applications from resource exhaustion attacks when processing untrusted archive files.

critical

How unsafe eval() code execution happens in JavaScript game scripting and how to fix it

A critical arbitrary code execution vulnerability was discovered in `scripts/CommandBlock.js` where user-provided input from a text dialog was directly concatenated into an `eval()` call without any sanitization or sandboxing. The fix replaces the dangerous `eval()` with a `new Function()` constructor, which provides better scope isolation and eliminates the string concatenation injection vector.

critical

How hardcoded API key exposure happens in JavaScript and how to fix it

A critical security vulnerability was discovered in `js/douban.js` where the Douban API key (`0ac44ae016490db2204ce0a042db2916`) was hardcoded directly in client-side JavaScript. This exposed the API credential to any user who could view the page source or use browser developer tools. The fix removed the hardcoded key, preventing unauthorized API access and potential abuse.

critical

How command injection via execSync() happens in Node.js CLI tools and how to fix it

A critical command injection vulnerability was discovered in `packages/core/bin/cli.js` where the `copyToClipboard` function used `execSync()` with shell command strings. Combined with insufficient filename sanitization in the cache functions, an attacker could inject arbitrary shell commands through malicious repository data containing shell metacharacters. The fix replaces `execSync()` with `execFileSync()` and tightens input sanitization on cache file paths.

critical

How buffer overflow in Intel SGX enclave ECALLs happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in Intel SGX enclave functions `ecall_encrypt_data` and `ecall_decrypt_data` in `backend/sgx/enclave/enclave.c`. The functions performed memory operations without validating that the provided buffer lengths matched the actual allocated buffer sizes, allowing an attacker controlling the untrusted application to trigger heap corruption within the secure enclave by passing oversized length parameters.