Guide · Backend
Laravel's October 2026 Token Guard Bypass Explained: Validate Input Types
15 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The Laravel token guard auth bypass let an unauthenticated caller act as a real user by sending a non-string api_token, such as an array value. The fix ships in laravel/framework 12.68.0 and 13.29.0.
- The token guard is Laravel's built-in driver that logs a request in by matching an api_token value against a column in your users table. It is the 'token' driver in config/auth.php.
- The bug bites on MySQL and MariaDB. Those engines compare a text column to a number by converting the text. A token that starts with a letter becomes 0, so the lookup can match an existing user.
- The fix is tiny: the guard now checks is_string() before it queries, so arrays and other non-string values are rejected. Upgrade and then check config/auth.php for any 'token' guard.
- Beginners can stop after the fix section. Engineers get the tested reason the match happens, the exact binding that reaches the database, and why the same request fails on PostgreSQL.
What goes wrong when a login check trusts request input?
A login check goes wrong when it takes whatever the request sends and drops it straight into a database lookup, without checking the shape of that value first. A token is meant to be text. If the code never confirms that, an attacker can send something that is not text at all.
That is the heart of the Laravel token guard auth bypass. The guard read the api_token from the request and looked it up, but it only checked that the value was not empty. An array like a single-element list is not empty, so it slipped through.
Picture a common setup. A logistics firm exposes an internal API where each call carries an api_token to identify the caller. Each request is logged in by matching that token to a row in the users table. The setup is made up, but the bug is real.
On a normal day every client sends its real token and the right user is loaded. The trouble starts when a caller sends a value of the wrong type, an array holding a number instead of a token string. The guard still runs the lookup, and on MySQL the lookup returns a user it should never have matched.
The request carried no valid token, so the fault is not in the database. It is in how the guard handled a value that was not a string, and in how MySQL compares that value to a text column.
Which words do you need before this bug makes sense?
Five short terms make the rest of this guide easy. Each one gets a plain meaning here.
Token guard. This is Laravel's built-in authentication driver that logs a request in by its api_token. You turn it on by setting a guard's driver to 'token' in config/auth.php.
api_token. This is the secret string a client sends to prove who it is. The guard matches it against a token column in your users table to find the matching user.
Bound parameter. This is a placeholder in a query, filled with a value at run time. Binding keeps the value apart from the SQL text, which is safer than pasting it in.
Type juggling. This is when a language or database quietly converts one type to another during a comparison, such as turning the text 'abc' into the number 0. The comparison then uses the converted value.
is_string. This is a PHP function that returns true only when a value is a string. The fix uses it to reject any token that is not plain text before the lookup runs.
What is the Laravel token guard auth bypass?
The Laravel token guard auth bypass is a critical flaw in the framework's token authentication driver, disclosed on 19 October 2026 as advisory GHSA-f9x3-7vq9-cph6. The guard accepted a non-string api_token and passed it into the user lookup. On MySQL and MariaDB the query could then match a user the caller had no token for.
The advisory rates it 9.2 on the CVSS version 4 scale, in the critical band. The attack needs no login and no user action, and it can let an unauthenticated caller act as an existing user on any route the token guard protects. The advisory credits the reporter by the GitHub handle KIKOmanasijev, whom Laravel News names as Hristijan Manasijev.
The vulnerable code checked only that the token was not empty. An array value passes an emptiness check, because a list with one item is not empty. So the guard went ahead and looked that array up as if it were a real token.
One point needs care. The GitHub advisory shows no CVE, while some news write-ups cite CVE-2026-114509. That CVE record does exist on the MITRE register, published on 21 October 2026, and it points back to this advisory and fix. Treat the GHSA id as the primary name and the CVE as a secondary one.
Why does a non-string api_token match a user on MySQL?
Because MySQL compares a text column to a number by turning the text into a number first. It reads only the leading digits, so a token that starts with a letter becomes the number 0. The lookup then asks for tokens equal to 0, and every such token qualifies.
To see how the value reaches the query, follow the path inside the guard. The guard reads api_token from the request. When that value is an array, Laravel's user provider does not run a plain equals clause. It runs an IN clause instead, and fills its bound parameter with each item, keeping that item's own type.
Two details make the value dangerous. The array wrapper gets it past the old emptiness check, since a one-item list is not empty. The item inside has to be a real number, not text, and a request body parsed as JSON keeps a number as a number. A plain text '0' would not trigger the match.
I checked this on a scratch database with made-up rows, on MySQL 8.0.46 and MariaDB 10.11.14. A query of the form WHERE api_token IN (0), with 0 bound as an integer, matched every row whose token started with a letter. The same value bound as the text '0' matched nothing.
-- Two rows, each with a token that starts with a letter (made up, scratch DB).
-- A number bound as an integer matches BOTH, because MySQL reads each
-- letter-led token as 0:
SELECT id, name FROM users WHERE api_token IN (0); -- matched 2 of 2 rows
-- The SAME digit sent as text matches nothing, as you would expect:
SELECT id, name FROM users WHERE api_token IN ('0'); -- matched 0 rows
-- Why: the engine casts the text token to a number, and a string that
-- starts with a letter casts to 0:
SELECT CAST('Yq8Lf2Rk9Tp4Wz1' AS SIGNED) AS n, -- n = 0
('Yq8Lf2Rk9Tp4Wz1' = 0) AS matches_zero; -- matches_zero = 1
-- A token that starts with 1 to 9 keeps its leading digits instead:
SELECT CAST('5Xyz9Lp1Qw3' AS SIGNED) AS n; -- n = 5, so no match with 0That last line is the whole bug in miniature. The token and the number 0 are not equal in any real sense, yet MySQL reports them as equal, because it compared them as numbers. So a request that carried no real token still loaded a user.
One detail narrows this. A token that starts with a digit from 1 to 9, such as 5Xyz, casts to 5, not 0, so it does not match. Tokens that start with a letter or with 0 do match, and most random tokens start with a letter.
The difference between a number and text is what matters. When the bound value is the integer 0, MySQL does the numeric conversion and the match succeeds. When it is the text '0', MySQL compares text to text and the match fails. A non-string token is what pushes the value onto the numeric path.
How a non-string token reaches the wrong user
Drag to exploreNotice the shape change at step 2: an array value turns the lookup into an IN clause with a number, which MySQL matches by type juggling.
Diagram connections:
- Request to Token guard (api_token is an array)
- Token guard to User provider (not empty, so look up)
- User provider to MySQL or MariaDB (where token in (0))
- MySQL or MariaDB to Wrong user loaded (0 matches letter-led tokens)
- Request: From client to guard
- Token guard: From guard to provider
- User provider: From provider to query
- MySQL or MariaDB: From query to row
- Wrong user loaded: Caller now acts as them
| api_token sent | Old guard check | On MySQL or MariaDB | On PostgreSQL |
|---|---|---|---|
| A real string token | Passes | Matches that one user | Matches that one user |
| Missing or empty | Blocked by empty check | No query runs | No query runs |
| A plain number like 0 | Blocked by empty check | No query runs | No query runs |
| An array such as a one-item list | Passes, it is not empty | Can match an existing user | No match or an error |
Why does the same request fail on PostgreSQL?
Because PostgreSQL does not quietly convert text to a number to make a comparison work. It treats a text column and an integer as different types and refuses to compare them, or it compares the value as text and finds no match. Either way, no wrong user is loaded.
I ran the same lookup against PostgreSQL 16.15. A query comparing the text token column to a literal integer failed with the error operator does not exist: character varying = integer. A version where the value arrived as text simply returned zero rows. The non-string token led nowhere.
This is why the advisory scopes the bypass to certain MySQL and MariaDB setups. The flaw in the guard is the same everywhere, but only the loose numeric comparison in MySQL and MariaDB turns that flaw into a login as the wrong user. On strict engines the bad request fails safely.
How do you fix the bypass in Laravel 12.68.0 and 13.29.0?
The real fix is to upgrade laravel/framework to 12.68.0 or 13.29.0, or any later release. The patched guard checks that the token is a string before it runs the lookup. A non-string value is now rejected, so no bad query reaches the database.
The is_string gate the fix adds
Drag to exploreNotice the new check: a string flows to the lookup, while any other type is turned away before a query runs.
Diagram connections:
- api_token value to is_string and not empty? (check the type)
- is_string and not empty? to Run the lookup (yes, it is a string)
- is_string and not empty? to Return no user (no, reject it)
- api_token value: From request to guard
- is_string and not empty?: The check the fix adds
- Run the lookup: Only a real string token
- Return no user: Array or other type
The change is small. In the guard's user method, the condition that was a bare emptiness check now also requires a string. The validate method gains a matching check that returns false for any non-string token. The framework added two regression tests that send an array as the api_token and confirm the guard returns no user.
// Vulnerable (before 12.68.0 / 13.29.0): only checks for empty
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}
// Fixed (12.68.0 and 13.29.0): a string is now required first
if (is_string($token) && ! empty($token)) {
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}After you upgrade, confirm the installed version and scan for the affected driver. The checks below are safe to run in any project directory and show you exactly where you stand.
# Show the installed framework version (expect 12.68.0+ or 13.29.0+)
composer show laravel/framework | grep versions
# Compare installed packages against published advisories
composer audit
# Find any guard still using the token driver
grep -n "'driver' => 'token'" config/auth.phpUpgrade the framework
Run composer update laravel/framework to reach 12.68.0 or 13.29.0 or later.
Find token guards
Search config/auth.php for any guard whose driver is 'token', and treat MySQL or MariaDB apps first.
Confirm the version
Run composer show laravel/framework and check the number is at or above the patched release.
Add your own check
If you cannot upgrade at once, validate that the incoming token is a string before any lookup, as a stopgap.
This upgrade breaks nothing for honest callers. A real token is a string, so it still passes. Only values that were never valid tokens, like arrays, are now turned away. The two framework tests cover exactly that case.
How do you stop this bug class in your own code?
You stop it by treating request input as untyped until you check it. The bug class is type confusion: a value of an unexpected type reaches a query, and the database does something surprising with it. The cure has three parts, and none of them is specific to Laravel.
Validate the type before the value reaches a query
Decide the type you expect and reject anything else. For a token, require a string and a sensible length. A framework request can return a string, an array or null depending on what was sent, so never assume the type that arrives is the type you wanted.
Compare hashed tokens, not raw ones
Store a hash of each token and compare hashes, not the plain value. A hash is always a fixed-length hex string, so a non-string input cannot sneak into the comparison as a number. In this bug, setups with token hashing turned on hit an error instead of a bypass, because hashing a non-string value fails outright.
Mind how arrays become SQL
Know what your query builder does with an array value. Many builders turn an array into an IN clause and bind each item. If one of those items is a number and your column is text, a loose database can match rows you did not intend. Bind the type you mean.
You can check your own guard quickly. Send a request where the token field is an array rather than a string, against a test environment, and confirm the response is a clean rejection, not a logged-in user. A patched Laravel app returns no user for that input.
How do you explain the Laravel token guard auth bypass in a system design interview?
Keep the story to four beats: the input, the missing check, the database quirk, and the fix. Then show you know the general lesson about types crossing a trust boundary.
Name the input
Say the token guard read api_token from the request and only checked that it was not empty, so an array slipped through.
Show the lookup
An array value became an IN clause with the number 0 bound as an integer, sent to the users table.
Explain the match
On MySQL and MariaDB, a token that starts with a letter casts to 0, so the clause matched the first such user.
Give the fix
The guard now requires is_string before the lookup, and the real fix for a team is to upgrade the framework.
Questions people ask
- Which Laravel versions fix the token guard auth bypass?
- The fix is in laravel/framework 12.68.0 and 13.29.0, and in any later release on those branches. Both releases are tagged 25 August 2026, but their release notes do not mention the fix. The advisory followed on 19 October 2026. So apps already on 12.68.0 or above are safe, even if the changelog looked routine. Versions below 12.68.0 and below 13.29.0 are affected.
- Does this affect Laravel Sanctum or Passport?
- Laravel News states that Sanctum and Passport register their own guard drivers and are not listed in this advisory, which is about the built-in token guard. If you use the 'token' driver directly in config/auth.php, you are in scope. Even so, upgrading the framework is the clean move, since it is one command and covers other fixes too.
- Is my app safe because it runs PostgreSQL?
- The wrong-user match was reproduced on MySQL and MariaDB, and not on PostgreSQL, which refuses or fails the loose comparison. But do not rely on that alone. Deployments change engines, and the guard itself was still accepting a bad type. Upgrade and validate the token type in your own code so the app is safe on any engine.
- Does this bug let an attacker read or change data directly?
- Not by itself. The flaw lets an unauthenticated caller be treated as an existing user on routes the token guard protects. What that enables depends on what the matched user can do. The advisory rates it 9.2, critical, because account takeover on a protected API can be severe. The lookup itself only reads a row.
- Is there a CVE for the Laravel token guard auth bypass?
- The GitHub advisory GHSA-f9x3-7vq9-cph6 is the primary identifier and showed no CVE on its page as of 23 October 2026. MITRE has since published CVE-2026-114509 for the same issue, dated 21 October 2026, pointing back to the advisory and fix commit. Use the GHSA id first and confirm the CVE on the MITRE register before quoting it.
- What is the smallest change that fixes it in code?
- The framework fix adds an is_string check in the guard's user and validate methods, so a non-string token is rejected before the database lookup. In your own code the same idea applies: confirm a token is a string of the expected length before you query. Comparing stored hashes rather than raw values adds another layer of safety.
Sources and further reading
- GitHub advisory GHSA-f9x3-7vq9-cph6 (Laravel token guard auth bypass)
- Fix commit 9b21ce0a9b (adds is_string checks and tests)
- Laravel News: Critical Token Guard Auth Bypass Fixed in 13.29 and 12.68 (20 Oct 2026)
- MITRE CVE record CVE-2026-114509
- laravel/framework v12.68.0 release page (25 Aug 2026)
- laravel/framework v13.29.0 release page (25 Aug 2026)