Feb 24, 2026·6 min read·31 visits
Craft CMS failed to atomically validate token usage limits. Attackers can race simultaneous requests to reuse 'single-use' tokens (like admin impersonation links) before the database updates. Fixed in 4.16.19 and 5.8.23 via Mutex locking.
A classic Time-of-Check Time-of-Use (TOCTOU) race condition in Craft CMS allows attackers to bypass usage limits on sensitive tokens. By flooding the server with concurrent requests, a single-use administrative impersonation token can be reused multiple times, leading to potential privilege escalation and unauthorized access.
In the world of web security, "once" is a very difficult number to enforce. We build systems that rely on the concept of ephemeral access: password reset links, email verification codes, and the holy grail of Craft CMS workflow—User Impersonation Tokens.
These tokens are the digital equivalent of a "Self-Destructs in 5 Seconds" tape. The developer promises that a token can be used exactly N times (usually 1). Once consumed, it vanishes into the ether, preventing replay attacks or accidental reuse. It is a simple contract: Check Limit -> Permit Access -> Increment Counter.
But here is the dirty secret of web development: between the "Check" and the "Increment," time exists. And where time exists, hackers exist. CVE-2026-27128 is a pristine example of what happens when you trust PHP to handle concurrency without adult supervision. It turns out, if you scream at Craft CMS loud enough (and in parallel), it forgets how to count.
The vulnerability is a textbook Time-of-Check Time-of-Use (TOCTOU) race condition located in the Tokens service. This is CWE-367 in its purest form.
The logic flaw occurred because the application treated a database transaction as a casual conversation rather than a strictly serialized event. Here is the sequence of events that the code thought was happening:
usageCount < usageLimit? (e.g., is 0 < 1?).usageCount or delete the token.In a single-threaded universe, this is fine. But the web is not single-threaded. When an attacker sends 50 requests simultaneously (using HTTP/2 multiplexing or just a really fast script), all 50 requests might hit Step 1 and Step 2 before the first request reaches Step 4.
Since the database hasn't been updated yet, all 50 threads see usageCount = 0. All 50 threads pass the check. All 50 threads grant access. By the time the database catches up, the damage is done. The "single-use" token has just been used 50 times.
Let's dissect the vulnerable code in src/services/Tokens.php. This is a simplified view of the crime scene before the fix.
// The Vulnerable Logic
$result = (new Query())
->select(['id', 'usageLimit', 'usageCount'])
->from([Table::TOKENS])
->where(['token' => $token])
->one();
// THE GAP IS HERE
// Thousands of CPU cycles pass between the SELECT above
// and the logic below.
if ($result['usageLimit']) {
// The fatal check: PHP is checking stale memory
if ($result['usageCount'] < $result['usageLimit']) {
// Access Granted!
$this->incrementTokenUsageCountById($result['id']);
} else {
// Too late (in theory)
$this->deleteTokenById($result['id']);
}
}The fix implemented in commit 3e4afe18279951c024c64896aa2b93cda6d95fdf introduces a Mutex (Mutual Exclusion) lock. This forces the requests to form a single-file line. Request B cannot even check the token until Request A has finished updating it.
// The Fix (Mutex Implementation)
$mutex = Craft::$app->getMutex();
$lockKey = "token:$token";
// Try to acquire lock for 5 seconds
if (!$mutex->acquire($lockKey, 5)) {
return false; // Go away if busy
}
try {
// Re-fetch logic happens INSIDE the lock
// ... validation ...
} finally {
$mutex->release($lockKey);
}> [!NOTE]
> While a Mutex works, a more performant database-native solution would have been an atomic update: UPDATE tokens SET usageCount = usageCount + 1 WHERE token = ? AND usageCount < usageLimit. If the affected rows = 0, the token is dead. No Mutex required. But hey, a patch is a patch.
To exploit this, we don't need complex memory corruption exploits or ROP chains. We just need speed.
asyncio script.We configure the attack to hold back the TCP connection establishment and send the last byte of the HTTP request for 20 threads simultaneously (the "Last-Byte Sync" technique).
In a successful exploitation, the attacker ends up with multiple valid session cookies for the target user, effectively bypassing the security control that was supposed to limit exposure to a single click.
Why is getting 5 sessions better than 1? Persistence and stealth.
If I steal a single-use token and use it, the legitimate user might click the link 5 minutes later, get an error, and alert IT. "Hey, my link didn't work."
However, if I race the condition, I can consume the token and potentially leave it in a state where the legitimate user might still squeeze in (depending on how the race resolves on the delete), or more likely, I establish multiple sessions on different devices. One session is for active browsing; the others are backups in case I get kicked out.
Furthermore, in complex workflows where tokens trigger actions (like "Approve Expense Report" or "Deploy to Production"), replaying the token could trigger the business logic multiple times, leading to financial loss or corruption of state.
The official fix relies on Craft's Mutex component. This is effective, but it comes with a major infrastructure caveat that many sysadmins miss.
The Trap: If your Craft CMS application is load-balanced across multiple servers (e.g., AWS Autoscaling Group) and you are using the default FileMutex, you are still vulnerable. The file lock only exists on the local server's filesystem. Request A hits Server 1 (locked), but Request B hits Server 2 (unlocked). Race condition succeeds.
The Proper Fix:
config/app.php is using RedisMutex, MysqlMutex, or PgsqlMutex for distributed locking.// config/app.php
'components' => [
'mutex' => [
'class' => \craft\mutex\Mutex::class, // Ensure this maps to a distributed driver
],
],If you cannot patch immediately, you can mitigate this at the WAF level by strictly rate-limiting endpoints like actions/users/login-by-token, though this is a fragile bandage at best.
CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | >= 4.5.0-RC1, < 4.16.19 | 4.16.19 |
Craft CMS Pixel & Tonic | >= 5.0.0-RC1, < 5.8.23 | 5.8.23 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-367 |
| Attack Vector | Network (Race Condition) |
| CVSS Score | 6.9 (Medium) |
| Impact | Security Bypass / Privilege Escalation |
| Affected Component | Tokens Service (getTokenRoute) |
| Exploit Status | PoC Available (Theoretical) |
The product checks the state of a resource before using it, but the resource's state can change between the check and the use in a way that invalidates the results of the check.
An information disclosure vulnerability exists in the Micronaut Framework's HTTP client components. The client fails to clear sensitive authorization headers and cookies when following redirects across different origins. If an application using the vulnerable client communicates with an endpoint that issues a redirect to an external host, the client will forward the original credentials, leading to potential token theft and session hijacking.
GHSA-52vm-mxx8-f227 is a dual-vector security flaw in phantom-audio (<= 1.3.0). The vulnerability allows arbitrary file writes due to unconfined Model Context Protocol (MCP) tool paths when the PHANTOM_OUTPUT_DIR environment variable is not defined. Concurrently, the platform lacks validation controls during the decompression of highly compressed audio files, resulting in resource-exhaustion denial of service and downstream parsing vulnerability exposure.
An authenticated path traversal and arbitrary local file read vulnerability exists in Craft CMS versions 4.x up to 4.17.6 within the assets/icon endpoint and Assets helper classes. By exploiting this vulnerability, an authenticated user can traverse directories and read arbitrary .svg files on the server's filesystem, or execute Stored Cross-Site Scripting (XSS) if they can upload a malicious SVG.
CVE-2026-56382 is a high-severity remote code execution vulnerability in Craft CMS versions 5.5.0 through 5.9.13. The vulnerability exists within the FieldsController::actionRenderCardPreview() method due to a lack of sanitization of the user-supplied fieldLayoutConfig configuration array, permitting authenticated administrators to register arbitrary PHP callbacks using Yii2 event handler injection mechanisms. This issue has been fully remediated in version 5.9.14.
An insecure direct object reference (IDOR) and missing authorization validation check in the Gittensory REST API and Model Context Protocol (MCP) server allowed authenticated users to query arbitrary miner profiles, exposing sensitive cryptographic hotkeys and daily financial/economic yields.
Code16 Sharp versions from 9.0.0 up to (but not including) 9.22.3 are vulnerable to a missing authorization flaw in the Quick Creation Command feature. The ApiEntityListQuickCreationCommandController fails to validate entity-level 'create' policies before returning administrative form designs or processing database modifications. Authenticated users with restricted access can bypass policy boundaries to access creation configurations and insert records.