Laravel TokenGuard bypass on MySQL lets api_token [0] log in as another user
Contents
TL;DR
Last updated October 21, 2026. Advisory GHSA-f9x3-7vq9-cph6 published October 19, 2026; no CVE assigned yet
What happened An unauthenticated attacker could be authenticated as an existing user without that user’s API token (CVSS v4.0 9.2, Critical)
Who is affected Laravel apps with a guard using 'driver' => 'token' in config/auth.php and the user table on MySQL or MariaDB. Sanctum and Passport are not covered by the advisory
What to do
- Check the installed version with
composer show laravel/framework - Upgrade to 12.68.0+ (12.x) or 13.29.0+ (13.x). Both shipped on August 25, 2026
- Run
composer auditand look for GHSA-f9x3-7vq9-cph6
A bug in Laravel’s built-in token authentication (TokenGuard) let a request be authenticated as an existing user without that user’s token.
The GitHub advisory GHSA-f9x3-7vq9-cph6 was published on October 19, 2026, rated 9.2 (Critical) on CVSS v4.0.
As of October 21, 2026, no CVE ID has been assigned.
The reporter, Hristijan Manasijev, wrote in a post on X that on MySQL/MariaDB an unauthenticated attacker could log in as an existing victim user.
The fixed releases, laravel/framework 12.68.0 and 13.29.0, came out on August 25, 2026, almost two months before the advisory (v12.68.0, v13.29.0).
The details were published after the fix had been out for a while, so an app updated since late August is usually already on a fixed version.
Version constraints in composer.json can block the upgrade, though.
Confirm the installed version with composer show.
Affected configuration
In Laravel, a guard is the component that decides who sent a request, and config/auth.php lists each guard under guards with the method (driver) it uses.
A guard with driver set to token is the simplest built-in method: it compares the token string in the request with the api_token column of the users table.
The advisory says the bug occurs with this standard token guard when the database is MySQL or MariaDB.
| Item | Affected | Not affected |
|---|---|---|
| Guard driver | token | session, Sanctum (sanctum), Passport (passport) |
| Database | MySQL, MariaDB | The advisory does not list other databases as a condition |
| laravel/framework (13.x) | Before 13.29.0 | 13.29.0 and later |
| laravel/framework (12.x) | Before 12.68.0 | 12.68.0 and later |
Sanctum is Laravel’s official API token package and Passport is its OAuth2 server package; most current Laravel APIs use one of them.
Both register their own guard drivers, so they are outside this advisory (Laravel News).
The token guard shipped as the api guard in the config/auth.php of new-project skeletons up to Laravel 7 (laravel/laravel 7.x branch).
It is gone from the 8.x skeleton, but a project created on Laravel 7 or earlier and upgraded since then may still carry it.
If you have routes behind the auth:api middleware and the api guard looks like this, the app is in scope.
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
Checking your app
From the project root, check whether a token guard exists and which laravel/framework version is installed.
grep -n "'driver' => 'token'" config/auth.php
composer show laravel/framework | grep versions
composer audit
composer audit checks installed packages against published security advisories, and an affected version lists GHSA-f9x3-7vq9-cph6.
To check only composer.lock without a vendor directory, use composer audit --locked.
Update with composer update laravel/framework; 12.68.0 or later on 12.x, or 13.29.0 or later on 13.x, contains the fix.
Why an array containing 0 matches a token
Before the fix, TokenGuard only checked that the token was not empty before passing it to the database lookup.
In the pre-fix v13.28.0 code, it looks for the token in the query string, then the request body, then the Authorization: Bearer header (TokenGuard.php).
A JSON body does not have to send a string; {"api_token": [0]} delivers an array.
PHP’s empty() treats the string "0" as empty, but [0] is an array with one element, so it is not empty and goes straight into the lookup.
EloquentUserProvider, which finds the user, builds the condition with whereIn() when the value is an array and where() otherwise (EloquentUserProvider.php).
With [0], the query looks for rows whose api_token equals the integer 0, and first() limits it to one row.
select * from users where api_token in (0) limit 1
When MySQL compares a string with a number, it converts both to floating-point numbers, and a string that does not start with a digit becomes 0 (MySQL reference manual).
The manual shows SELECT 0 = 'x6'; returning true and a VARCHAR column searched with = 0 returning every row, and notes that this happens even in strict SQL mode.
Any user whose api_token converts to 0, such as one starting with a letter, therefore matches in (0), and TokenGuard treats the single returned user as authenticated.
A token starting with a nonzero digit, like 1abc, converts to a different number and does not match.
flowchart TD
A["Send api_token as [0] in JSON"] --> B["TokenGuard: not empty, passes"]
B --> C["EloquentUserProvider: array, uses whereIn"]
C --> D["MySQL: compares api_token with integer 0 numerically"]
D --> E["Tokens that convert to 0 match"]
E --> F["Authenticated as the one returned user"]
The query has no ordering, so the database decides which matching user comes back, and the attacker cannot pick the victim.
The advisory says an attacker without a token can be authenticated as another user and take over that account on any route protected by the affected guard.
With 'hash' => true in config/auth.php, the database stores a SHA-256 hash of the token, and the pre-fix code also runs the incoming token through hash('sha256', $token) before the lookup.
In PHP 8, hash() throws a type error when given an array, so from the code, this setting should stop before the query runs.
The advisory does not separate cases by the hash setting, though, so apps with hashing enabled still need the upgrade.
The fix
The fix commit (9b21ce0) adds an is_string() check to user() and validate() in TokenGuard; the change to the guard itself is two lines.
- if (! empty($token)) {
+ if (is_string($token) && ! empty($token)) {
- if (empty($credentials[$this->inputKey])) {
+ if (! is_string($credentials[$this->inputKey] ?? null) || empty($credentials[$this->inputKey])) {
Array and numeric tokens are now rejected before they reach the database, and authentication fails.
The same commit adds a test asserting that user() returns null when api_token is [0].
The advisory only lists fixed versions for 12.x and 13.x, but TokenGuard in 11.x v11.56.1 (tagged August 25, 2026) has the same is_string() check.
The 10.x and 9.x branches, as of October 21, 2026, still have only the old ! empty($token) check.
The advisory offers no workaround other than upgrading.
Going by what the fix changes, an app that cannot upgrade could reject non-string api_token values in its own middleware until it moves the token guard to Sanctum or another method.
A second advisory published the same day: JSON:API resources
Hristijan Manasijev also reported a medium-severity advisory, GHSA-5wcm-3v2w-8362 (CVSS v3.1 6.5), published the same day.
It affects the JSON:API resources added in Laravel 12.45: a request to a read-only endpoint could delete the contents of a related table. It is fixed in 12.68.0 and 13.27.0.