Feb 12, 2026·5 min read·11 visits
In worker mode, FrankenPHP < 1.11.2 failed to clear `$_SESSION` between requests. If a script accessed session data before calling `session_start()`, it would read the previous user's session from memory. Fixed in 1.11.2.
FrankenPHP, the modern application server that brings Go-like performance to PHP, suffered from a critical session isolation flaw in its worker mode. By failing to correctly scrub the global symbol table between requests, the server allowed the `$_SESSION` superglobal to persist across different HTTP requests handled by the same worker thread. This effectively turned the server into a game of Russian Roulette where one user's private data could be served to the next user merely by luck of the draw.
FrankenPHP is the cool kid on the block. It embeds the PHP interpreter directly into Caddy (a Go-based web server), allowing PHP to run like a long-lived application rather than the traditional "die-after-every-request" CGI model. This is called Worker Mode, and it’s blazing fast. It bypasses the repetitive initialization overhead of standard PHP-FPM.
But here's the catch: When you keep the PHP engine alive, you become responsible for taking out the trash. In the standard model, the OS destroys the process memory, guaranteeing a clean slate for the next request. In worker mode, variables, globals, and static properties persist in memory unless explicitly reset.
If the server logic forgets to scrub even one superglobal, that data sits in RAM, waiting for the next request to stumble upon it. It’s like a hotel implementation where the maid changes the towels but forgets to change the sheets between guests. CVE-2026-24894 is exactly that: a dirty sheet left on the bed of a high-performance application server.
The vulnerability lies in how FrankenPHP handles the $_SESSION superglobal during its "reset" phase. PHP's Zend Engine uses a global symbol table (EG(symbol_table)) to track variables. When a request ends in worker mode, FrankenPHP runs a function to wipe the slate clean. It diligently cleared $_GET, $_POST, $_COOKIE, and others.
However, it missed one critical VIP: $_SESSION.
The PHP Session extension's request shutdown logic (RSHUTDOWN) does decrement the reference count of the session array, but—and this is the crucial technical nuance—it does not remove the string key _SESSION from the symbol table. It just leaves it there, dangling or pointing to the old data structure until the next session_start() call overwrites it.
This creates a race condition of logic. If the next request (processed by the same worker thread) accesses $_SESSION before initializing its own session, it doesn't get null or an empty array. It gets the full, unredacted session array of the previous user. This is a classic "object reuse" vulnerability, but at the scope of the entire HTTP request context.
The fix is embarrassingly simple, as most critical security patches are. The developers had to manually intervene in the Zend Engine's symbol table to force the eviction of the session variable.
Here is the essence of the patch applied in commit 24d6c991a7761b638190eb081deae258143e9735. Notice the explicit deletion call added to frankenphp.c:
static void frankenphp_reset_super_globals() {
// ... existing resets for $_GET, $_POST ...
/*
* The Fix: explicitly nuke _SESSION from orbit.
* Unlike other superglobals, the engine doesn't auto-clean this
* deep enough for worker mode.
*/
zend_hash_str_del(&EG(symbol_table), "_SESSION", sizeof("_SESSION") - 1);
}Before this line was added, the _SESSION symbol persisted. By adding zend_hash_str_del, the server ensures that even if the PHP extension leaves artifacts behind, the variable itself is removed from the scope, forcing the next request to start fresh.
Exploiting this requires no authentication, no special headers, and no complex race conditions—just a busy server and a bit of luck (or volume). The attack relies on the victim and the attacker landing on the same worker thread sequentially.
Step 1: The Victim (Alice) Alice logs into the application. The app sets her user ID and a sensitive token in the session.
// Alice's Request
session_start();
$_SESSION['user_id'] = 42;
$_SESSION['api_token'] = 'x8s7df6...';
// Request ends, but worker keeps memory alive.Step 2: The Attacker (Bob) Bob sends a request to an endpoint that checks for session existence before starting a session. This is common in logging middleware, analytics scripts, or sloppy authentication checks.
// Bob's Request (landing on the same worker)
// session_start() has NOT been called yet.
if (isset($_SESSION['api_token'])) {
// This should be false. But thanks to the bug, it's TRUE.
log_hack("Leaked token: " . $_SESSION['api_token']);
}In a high-traffic environment, an attacker could simply spam requests to a "echo" endpoint and harvest session tokens, PII, or internal state variables from random users across the platform.
The CVSS score of 8.7 is justified. This isn't just a data leak; it's potential total account takeover. If your application stores authentication state, role permissions, or API keys in $_SESSION, an unauthenticated attacker can effectively "become" an administrator simply by hitting the server at the right millisecond.
Furthermore, because this happens in memory, there are no logs of the data transfer. The web server logs will show normal HTTP 200 OK responses. There is no intrusion signature, no SQL injection pattern, and no malware to scan for. It is a silent bleed of the application's most trusted memory segment.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
FrankenPHP Dunglas | < 1.11.2 | 1.11.2 |
| Attribute | Detail |
|---|---|
| CVSS Score | 8.7 (High) |
| Attack Vector | Network |
| CWE ID | CWE-200 (Info Exposure) / CWE-668 |
| Architecture | x86/ARM (Go/C) |
| Component | frankenphp_reset_super_globals |
| Exploit Status | PoC Available |
Exposure of Resource to Wrong Sphere