CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-27128
6.90.04%

Crafty Concurrency: Smashing the Token Limit in Craft CMS

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 24, 2026·6 min read·10 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: The Promise of One

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 Flaw: A Matter of Milliseconds

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:

  1. Select: Fetch the token from the database.
  2. Check: Does usageCount < usageLimit? (e.g., is 0 < 1?).
  3. Act: If yes, let the user in.
  4. Update: Increment 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.

The Code: The Smoking Gun

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.

The Exploit: Racing the Database

To exploit this, we don't need complex memory corruption exploits or ROP chains. We just need speed.

The Attack Scenario

  1. Target: An admin generates an Impersonation Token (a URL that logs you in as another user instantly).
  2. Interception: The attacker obtains this URL (via sniffing, logs, or social engineering).
  3. Weaponization: We use a tool like Burp Suite Turbo Intruder or a custom Python asyncio script.

The Race

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.

The Impact: Why This Matters

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 Fix: Mitigation & Caveats

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:

  1. Update: Patch to 4.16.19 or 5.8.23.
  2. Configure Mutex: Ensure 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.

Official Patches

Craft CMSGitHub Commit fixing the race condition

Fix Analysis (1)

Technical Appendix

CVSS Score
6.9/ 10
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
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Craft CMS 4.x < 4.16.19Craft CMS 5.x < 5.8.23

Affected Versions Detail

Product
Affected Versions
Fixed Version
Craft CMS
Pixel & Tonic
>= 4.5.0-RC1, < 4.16.194.16.19
Craft CMS
Pixel & Tonic
>= 5.0.0-RC1, < 5.8.235.8.23
AttributeDetail
CWE IDCWE-367
Attack VectorNetwork (Race Condition)
CVSS Score6.9 (Medium)
ImpactSecurity Bypass / Privilege Escalation
Affected ComponentTokens Service (getTokenRoute)
Exploit StatusPoC Available (Theoretical)

MITRE ATT&CK Mapping

T1367Exploit Race Conditions
Privilege Escalation
T1550Use Alternate Authentication Material
Defense Evasion
CWE-367
Time-of-check Time-of-use (TOCTOU) Race Condition

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.

Known Exploits & Detection

TheoryStandard TOCTOU exploitation using Burp Suite Turbo Intruder 'Last-Byte Sync' technique.

Vulnerability Timeline

Initial fix noted in changelog
2026-01-09
Patch commit authored
2026-01-16
CVE and GHSA Advisory published
2026-02-24

References & Sources

  • [1]GitHub Advisory GHSA-6fx5-5cw5-4897
  • [2]CWE-367: TOCTOU

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.