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 Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

critical

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote versions prior to 1.8.4, where unescaped line terminators allowed attackers to inject arbitrary shell commands through crafted input strings. The fix pins shell-quote to version 1.9.0 via a `package.json` overrides directive in the FabricExample project, ensuring all transitive dependencies resolve to the patched version. Left unaddressed, this vulnerability could have allowed arbitrary code execution on any

critical

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A critical Cross-Site Scripting (XSS) vulnerability was discovered in `js/main.js` where commit messages fetched from the GitHub API were directly interpolated into `innerHTML` without any sanitization. An attacker with repository write access could push a commit with a malicious message like `<img src=x onerror=alert(document.cookie)>`, causing arbitrary JavaScript execution in every visitor's browser. The fix applies HTML entity encoding to all five dangerous characters before rendering.

critical

How Cross-Site Scripting (XSS) happens in JavaScript template rendering and how to fix it

A cross-site scripting (XSS) vulnerability in `renderer/views/library.js` allowed attackers who could control mod metadata—such as category icons rendered in pack thumbnail grids—to inject arbitrary JavaScript through unescaped output in `innerHTML` assignments. The fix wraps the `catIcon()` return value in the existing `esc()` helper, ensuring all dynamically generated HTML content is properly encoded before insertion into the DOM.

critical

How Prototype Pollution Denial of Service Happens in Node.js HTTP Libraries and How to Fix It

A critical prototype pollution vulnerability in axios versions 1.12.0 and earlier could allow attackers to trigger denial of service attacks by poisoning the configuration object through the `__proto__` key. The vulnerability was fixed by upgrading axios to 1.13.5 and updating related dependencies like follow-redirects to 1.16.0, which implements stricter input validation in the mergeConfig function.