Laravel TokenGuard bypass: when a token isn't a string

$ cat blog/laravel-tokenguard-auth-bypass.md
✓ incident
✓ authentication
✓ PHP
✓ SAST
6 min read
$ 

Laravel's TokenGuard had an authentication bypass, CVE-2026-114509, rated CVSS 9.2. On MySQL or MariaDB, a crafted credential that was not a string could make the lookup match another user's record, so an unauthenticated attacker could act as that user. It was covered in The Hacker News' ThreatsDay roundup in October 2026.

What happened

According to The Hacker News, under MySQL or MariaDB "an attacker can submit a specially crafted, non-string credential value," which made the query "match an unintended user record rather than failing authentication." As a result, "an unauthenticated attacker may authenticate as another user without possessing that user's API token." That is account takeover on any route protected by the affected guard.

The fix is to upgrade to Laravel 13.29.0 or 12.68.0. The article does not list the full affected version range, and it does not publish the exact payload. We won't guess at either here. What follows explains the general class of bug, with our own example.

Why it worked

The class is type confusion in a credential lookup. Code expects the token to be a string. But request input in PHP is not always a string: JSON bodies can carry numbers, booleans, arrays and nulls, and query strings can carry arrays (token[]=...). If that value goes straight into a database query, its type can change what the query means.

MySQL makes this worse because it converts types when comparing. Compare a string column with a number, and MySQL converts the column's strings to numbers first. A string that doesn't start with digits converts to 0. So a comparison that was meant to be "token equals this exact secret" can become "token, read as a number, equals this number", and many rows may satisfy that. Which row comes back first is then up to the database.

This is a general explanation of the class. We don't know whether CVE-2026-114509 uses this exact coercion; the article doesn't say.

Here is an illustrative example, not Laravel's code:

<?php
// Illustrative only: a token guard that trusts the input's type (vulnerable)
public function user(Request $request): ?User
{
    $token = $request->input('api_token');   // string, int, bool, array or null

    if (empty($token)) {
        return null;
    }

    // The raw value goes into the query, and its type changes the comparison.
    return User::where('api_token', $token)->first();
}

Three things are wrong. The type of $token is never checked. The raw token is stored in the database, so the lookup compares secrets directly in SQL. And the database, not your code, decides what "equal" means.

A safer version checks the type first, keeps a SHA-256 digest of the token instead of the token itself, and compares in constant time:

<?php
// Illustrative only: check the type, look up by id, compare hashes (fixed)
public function user(Request $request): ?User
{
    $token = $request->input('api_token');

    // 1. Only a non-empty string of the expected shape is a credential.
    if (! is_string($token) || ! preg_match('/^\d+\|[A-Za-z0-9]{40,}$/', $token)) {
        return null;
    }

    // 2. Token format "<id>|<secret>": look up by id, never by the secret.
    [$id, $secret] = explode('|', $token, 2);
    $record = ApiToken::find((int) $id);

    // 3. Compare the stored hash with the hash of the secret, in constant time.
    if ($record === null || ! hash_equals($record->token_hash, hash('sha256', $secret))) {
        return null;
    }

    return $record->user;
}

Now the input's shape cannot change the query: the only value that reaches the database is an integer id, and the secret is checked in PHP with hash_equals. A leaked database row also no longer contains a usable token. If you use a maintained package for API tokens, it should already work this way; check rather than assume.

attack, as reported where OpenRouting checks 1 Attacker sends a non-string token✓ Auth agent: credential types 2 Guard looks up a user by that value✓ Semgrep PHP rules 3 MySQL matches an unintended userdatabase behaviour 4 Request runs as that user✓ PR review of auth changes 5 Account takeover on guarded routesfix: upgrade Laravel
The attack as reported, and where an OpenRouting check applies. Illustrative.

What to do now

  1. Upgrade Laravel to 13.29.0 or 12.68.0 (or later). This is the fix; everything else is defence in depth.
  2. Check your logs for requests to token-guarded routes where the credential was not a plain string (arrays, numbers, booleans in JSON bodies or query strings), if your logging captures that.
  3. In any custom guard or login code, reject a credential unless it is a non-empty string of the expected format, before it reaches the database.
  4. Store API tokens as hashes, look records up by a non-secret id, and compare with hash_equals.
  5. Validate request types at the edge (Laravel form requests with 'api_token' => ['required', 'string']), so type surprises fail early everywhere, not just in auth.
  6. If you suspect abuse, revoke and reissue API tokens for affected users.

How OpenRouting helps

OpenRouting can't patch Laravel for you, but it reviews the code you write around authentication, which is where this class of bug lives in your own app.

  • Semgrep's PHP and framework rules run on every repository scan and flag risky patterns such as raw input flowing into queries.
  • The authentication agent reviews auth-related findings with the surrounding code: unchecked credential types, tokens stored or compared in plain text, comparisons that aren't constant-time. AI triage, by Claude, via Amazon Bedrock, gives each finding a verdict, a plain-English explanation and a suggested fix. Secret values are redacted before any AI call.
  • PR review runs on the files a pull request changes, so a new custom guard or login change is reviewed before merge.
  • API collection rules check imported Postman, OpenAPI or HAR files for endpoints with no authentication, guessable ids and similar design problems, without sending any traffic.

What it won't catch

  • OpenRouting does not do dependency (SCA) scanning yet. It won't tell you that your Laravel version is affected; upgrade through Composer and follow the vendor advisory.
  • It reviews your code, not the framework's internals. A bypass inside a library you call is out of scope for a scan of your repository.
  • Static rules don't know your database's comparison rules. Some type-confusion bugs only show up with a particular database, so tests that send arrays, numbers and booleans to your auth endpoints are still worth writing.
  • Findings appear when a scan runs, from the dashboard or on a pull request review.

For the authorization side of the same problem, see broken access control and IDOR.

Sources

Questions

What is Laravel CVE-2026-114509?

It is an authentication bypass in Laravel's TokenGuard, rated CVSS 9.2. On MySQL or MariaDB, a crafted credential that was not a string could make the lookup match another user's record, so an unauthenticated attacker could act as that user.

Which Laravel versions fix CVE-2026-114509?

Upgrade to Laravel 13.29.0 or 12.68.0, or later. The article does not list the full affected version range, and it does not publish the exact payload.

Does CVE-2026-114509 rely on MySQL type coercion?

That isn't known; the article doesn't say. MySQL converting strings to numbers in comparisons is a general example of how a non-string credential can change what a query matches, not a confirmed description of this CVE.

How do I protect a custom token guard from type confusion?

Reject a credential unless it is a non-empty string of the expected format, before it reaches the database. Store API tokens as hashes, look records up by a non-secret id, and compare with hash_equals.

Keep reading