VYPR
Moderate severityNVD Advisory· Published Feb 24, 2026· Updated Feb 28, 2026

Craft CMS's race condition in Token Service potentially allows for token usage greater than the token limit

CVE-2026-27128

Description

Craft is a content management system (CMS). In versions 4.5.0-RC1 through 4.16.18 and 5.0.0-RC1 through 5.8.22, a Time-of-Check-Time-of-Use (TOCTOU) race condition exists in Craft CMS’s token validation service for tokens that explicitly set a limited usage. The getTokenRoute() method reads a token’s usage count, checks if it’s within limits, then updates the database in separate non-atomic operations. By sending concurrent requests, an attacker can use a single-use impersonation token multiple times before the database update completes. To make this work, an attacker needs to obtain a valid user account impersonation URL with a non-expired token via some other means and exploit a race condition while bypassing any rate-limiting rules in place. For this to be a privilege escalation, the impersonation URL must include a token for a user account with more permissions than the current user. Versions 4.16.19 and 5.8.23 patch the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A TOCTOU race condition in Craft CMS token validation allows repeated use of single-use impersonation tokens, potentially leading to privilege escalation.

A Time-of-Check-Time-of-Use (TOCTOU) race condition exists in Craft CMS’s token validation service for tokens with a limited usage count [1]. In the getTokenRoute() method, the token’s usage count is read, checked against the limit, and then updated in separate non-atomic operations, allowing a race window [4]. This affects versions 4.5.0-RC1 through 4.16.18 and 5.0.0-RC1 through 5.8.22 [1].

To exploit this, an attacker must first obtain a valid user account impersonation URL with a non-expired token via some other means [1]. By sending concurrent requests to that URL, the attacker can use the single-use token multiple times before the database update completes, bypassing rate-limiting rules if present [4]. For privilege escalation, the impersonation token must correspond to a user account with more permissions than the attacker’s current user [1].

If successful, an attacker can reuse a token beyond its intended limit, potentially gaining unauthorized access to higher-privileged user accounts or performing actions restricted to that user [1][4]. This could lead to data exposure, modification, or full system compromise depending on the target user’s permissions.

The vulnerability is patched in Craft CMS versions 4.16.19 and 5.8.23 [1]. The fix introduces a mutex lock around the token validation logic to ensure atomicity [2]. Users are strongly advised to upgrade to the patched versions immediately [4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
craftcms/cmsPackagist
>= 4.5.0-RC1, < 4.16.194.16.19
craftcms/cmsPackagist
>= 5.0.0-RC1, < 5.8.235.8.23

Affected products

2

Patches

1
3e4afe182799

Fixed GHSA-6fx5-5cw5-4897

https://github.com/craftcms/cmsbrandonkellyJan 16, 2026via ghsa
2 files changed · +33 22
  • CHANGELOG.md+1 0 modified
    @@ -5,6 +5,7 @@
     - Fixed an error that could occur if the `purgeStaleUserSessionDuration` config setting was set to a duration interval string. ([#18238](https://github.com/craftcms/cms/issues/18238))
     - Fixed XSS vulnerabilities. (GHSA-6j87-m5qx-9fqp, GHSA-3jh3-prx3-w6wc)
     - Fixed SSRF vulnerabilities. (GHSA-gp2f-7wcm-5fhx, GHSA-v2gc-rm6g-wrw9)
    +- Fixed a TOCTOU vulnerability. (GHSA-6fx5-5cw5-4897)
     
     ## 4.16.18 - 2026-01-09
     
    
  • src/services/Tokens.php+32 22 modified
    @@ -122,35 +122,45 @@ public function getTokenRoute(string $token): array|false
         {
             // Take the opportunity to delete any expired tokens
             $this->deleteExpiredTokens();
    -        $result = (new Query())
    -            ->select(['id', 'route', 'usageLimit', 'usageCount'])
    -            ->from([Table::TOKENS])
    -            ->where(['token' => $token])
    -            ->one();
    -
    -        if (!$result) {
    -            // Remove it from the request  so it doesn’t get added to generated URLs
    -            Craft::$app->getRequest()->setToken(null);
     
    +        $mutex = Craft::$app->getMutex();
    +        $lockKey = "token:$token";
    +        if (!$mutex->acquire($lockKey, 5)) {
                 return false;
             }
     
    -        // Usage limit enforcement (for future requests)
    -        if ($result['usageLimit']) {
    -            // Does it have any more life after this?
    -            if ($result['usageCount'] < $result['usageLimit'] - 1) {
    -                // Increment its count
    -                $this->incrementTokenUsageCountById($result['id']);
    -            } else {
    -                // Just delete it
    -                $this->deleteTokenById($result['id']);
    -
    -                // Remove it from the request as well so it doesn’t get added to generated URLs
    +        try {
    +            $result = (new Query())
    +                ->select(['id', 'route', 'usageLimit', 'usageCount'])
    +                ->from([Table::TOKENS])
    +                ->where(['token' => $token])
    +                ->one();
    +
    +            if (!$result) {
    +                // Remove it from the request  so it doesn’t get added to generated URLs
                     Craft::$app->getRequest()->setToken(null);
    +                return false;
                 }
    -        }
     
    -        return (array)Json::decodeIfJson($result['route']);
    +            // Usage limit enforcement (for future requests)
    +            if ($result['usageLimit']) {
    +                // Does it have any more life after this?
    +                if ($result['usageCount'] < $result['usageLimit'] - 1) {
    +                    // Increment its count
    +                    $this->incrementTokenUsageCountById($result['id']);
    +                } else {
    +                    // Just delete it
    +                    $this->deleteTokenById($result['id']);
    +
    +                    // Remove it from the request as well so it doesn’t get added to generated URLs
    +                    Craft::$app->getRequest()->setToken(null);
    +                }
    +            }
    +
    +            return (array)Json::decodeIfJson($result['route']);
    +        } finally {
    +            $mutex->release($lockKey);
    +        }
         }
     
         /**
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.