Vulnerabilities
Laravel token guard bypass: log in as a user with no token
GHSA-f9x3-7vq9-cph6 (CVSS 9.2) lets an unauthenticated request log in as a user on Laravel token guards backed by MySQL or MariaDB. Fixes and checks.
Laravel published advisory GHSA-f9x3-7vq9-cph6 on October 19, rated Critical with a CVSS v4 score of 9.2. An attacker with no account can be authenticated as an existing user without that user's API token. It applies to apps that define an auth guard with 'driver' => 'token' in config/auth.php and keep their users table on MySQL or MariaDB. The flaw was reported by Hristijan Manasijev, and CVE-2026-114509 was assigned on October 21.
Sanctum and Passport register their own guard drivers and are not covered. The fixed releases, laravel/framework 12.68.0 and 13.29.0, shipped on August 25, almost two months before the advisory, so an app that has taken a framework update since then is already patched. The ones at risk are older API backends that still carry the token guard from a Laravel 7 era skeleton and have not been updated since.
How it works
The token guard (Illuminate\Auth\TokenGuard) is Laravel's oldest API authentication option. Each user row has an api_token column, and the guard reads the token from the request: the api_token query parameter, then the request input, then a Bearer header or basic auth password. It passes that value to the user provider, which looks up the row whose api_token equals it.
Before the fix, the only check in user() was ! empty($token). Request input in Laravel includes a parsed JSON body, so the token does not have to be a string. A body of {"api_token": [0]} gives the guard a PHP array, and empty([0]) is false because the array has one element. The Eloquent user provider then sees an array value and builds a whereIn() clause, so the query becomes select * from users where api_token in (0) limit 1.
MySQL is where the integer matters. When MySQL compares a string with a number, it converts both to floating point, and a string that does not start with a digit converts to 0. A random token such as kX93... therefore equals 0, and the query returns a real user. The lilting.ch analysis says this happens even in strict SQL mode. That silent cast is why the advisory limits the condition to MySQL and MariaDB.
The attacker does not choose the victim. With no ORDER BY, the database returns whichever matching row it reaches first. In our reading that is usually the lowest primary key whose token starts with a letter, which on many apps is the first account created and often an admin. The attacker then acts as that user on every route protected by the guard.
The fix, commit 9b21ce0 by Taylor Otwell, changes the check to is_string($token) && ! empty($token) in user() and adds the same string test to validate(). Arrays and numbers are rejected before any query runs, and a new test confirms that api_token set to [0] returns no user.
Does the hash setting help?
The guard has a hash option. With 'hash' => true, the token goes through hash('sha256', $token) before the lookup, and in PHP 8 hash() throws a TypeError when given an array, so the request fails with a server error instead of logging anyone in. The lilting.ch write-up agrees but still recommends upgrading, since the advisory does not separate the two cases. The old Laravel skeletons shipped 'hash' => false, so most legacy apps are in the exposed configuration.
What attackers are doing
As of October 26, no vendor, CERT or news source reports exploitation, and the CVE record notes none confirmed. The payload is one JSON field and the fix commit with its tests is public, so treat any app that matches the conditions and has not been updated since August as a patch-today item.
Manasijev also reported GHSA-5wcm-3v2w-8362 (CVSS v3.1 6.5), published the same day: on the JSON:API resources added in Laravel 12.45, a request to a read-only endpoint could empty a related table. It is fixed in 12.68.0 and 13.27.0, so the upgrade below covers both.
What to do
1. Find out whether the app uses the token guard
From the project root:
grep -n "'driver' => 'token'" config/auth.php
php artisan tinker --execute="dump(config('auth.guards'))"
The second command shows the guards as the running app sees them. A match usually sits under a guard named api. Then confirm the database with grep DB_CONNECTION .env; mysql or mariadb puts you in scope. Routes using auth:api middleware, or whatever the guard is called, are the exposed ones.
2. Check the version and upgrade
composer show laravel/framework | grep versions
composer audit
composer audit lists GHSA-f9x3-7vq9-cph6 if the installed version is affected; use composer audit --locked on a checkout with no vendor directory. To upgrade within your major version:
composer update laravel/framework --with-dependencies
You need 12.68.0 or later on Laravel 12, or 13.29.0 or later on Laravel 13. If the constraint in composer.json pins an older minor, raise it (for example composer require laravel/framework:^12.68) and run your tests before deploying.
The advisory lists fixed versions only for 12.x and 13.x. Laravel 11 left security support on March 12, 2026, and 10.x and 9.x before that, so apps on those branches should not wait for an official patch.
3. If you cannot upgrade today
Move the API to Sanctum, which stores hashed personal access tokens and is not covered by the advisory. That is the lasting fix for apps stuck on old branches, but it means issuing new tokens to API clients.
As a stopgap, reject any request where api_token is not a string before the guard runs. In Laravel that is a small global middleware that returns 401 when the request carries an api_token input that fails is_string(). It mirrors the official fix but is not an official workaround. The same rule fits a web application firewall (WAF): block JSON bodies on API paths where api_token is an array, number or object. If the API only serves known clients, allow its paths only from their IP ranges at the load balancer.
4. Check for abuse
Most access logs do not record request bodies, so a past attack will not show the payload. Work from the other direction:
- For apps with
'hash' => true, searchstorage/logs/laravel.logforhash(): Argument #2 ($data) must be of type string, array givenwith a stack trace throughTokenGuard. Each one is a failed attempt. - In the web server access log, find API requests that succeeded (200 responses on
auth:apiroutes) but carried noapi_tokenquery parameter and noAuthorizationheader. If your own clients always send the token in the query string or a header, a request like that from an unfamiliar IP deserves a closer look. - Review API activity for the oldest accounts in the users table, starting with the lowest IDs and any admins: unfamiliar source IPs, changed email addresses or passwords, new data exports.
If you find signs of abuse, rotate every api_token, turn on 'hash' => true with hashed stored values while you plan the move to Sanctum, and reset credentials for the accounts that were used.
The wider lesson
The fix shipped silently in August and the advisory followed in October. Teams that only update a dependency when an advisory names it were exposed for two months without knowing, while teams that take regular minor releases were already safe. Running composer audit in CI on every build catches the advisory the day it is published, and a monthly composer update of minor versions picks up fixes like this one before anyone announces them. Legacy pieces such as the token guard belong on a retirement list with a date attached.
Our web penetration testing covers API authentication, including type confusion in token and login parameters like this one, and our red team code review reads guards and providers in the source. Open the chat and Yaali, our AI agent, will pass your question to the engineer who would do the work.
Sources: GitHub advisory GHSA-f9x3-7vq9-cph6, fix commit 9b21ce0, lilting.ch analysis, Laravel News, The Hacker News ThreatsDay bulletin, SecurityOnline CVE-2026-114509, MySQL type conversion in expression evaluation, Laravel release support policy.