critical

CVE

CVE-2026-114509, CVE-2026-114508

CWE

CWE-287, CWE-1287, CWE-470, CWE-863

Affected Surface

  • Composer package `laravel/framework` versions `< 12.68.0` and `>= 13.0.0, < 13.29.0` for `CVE-2026-114509` (`TokenGuard` non-string credential authentication bypass on MySQL/MariaDB)
  • Composer package `laravel/framework` versions `>= 12.45.0, < 12.68.0` and `>= 13.0.0, < 13.27.0` for `CVE-2026-114508` (`JsonApiResource` nested `include` relationship resolution)
  • Composer package `livewire/livewire` versions `>= 4.3.0, < 4.4.8` for `GHSA-mhf7-xp6r-6qrc` (`#[Authorize]` attribute on component actions, `#[On]` event listeners, and `#[Json]` methods)

TL;DR

On 23 October 2026, the GitHub Advisory Database published three reviewed advisories for Laravel (laravel/framework) and Livewire (livewire/livewire):

  1. CVE-2026-114509 (GHSA-f9x3-7vq9-cph6, CVSS v4.0 9.2 Critical) in laravel/framework (< 12.68.0 and >= 13.0.0, < 13.29.0): Illuminate\Auth\TokenGuard checked ! empty($token) without verifying is_string($token). When an attacker submits a non-string credential such as ?api_token[]=0 or {"api_token": [0]} against a standard unhashed token API guard backed by MySQL or MariaDB, EloquentUserProvider::retrieveByCredentials() binds 0 into WHERE api_token = ?. Under MySQL and MariaDB’s implicit string-to-integer comparison rules, any non-numeric token string (such as a random hex or Base64 token starting with a letter) coerces to 0 and matches on the first row, logging the unauthenticated caller in without a valid token.
  2. CVE-2026-114508 (GHSA-5wcm-3v2w-8362, Medium) in laravel/framework (>= 12.45.0, < 12.68.0 and >= 13.0.0, < 13.27.0): In JsonApiResource (ResolvesJsonApiElements), compileResourceRelationships() passed nested ?include= segments directly into $relatedModels->loadMissing($relations) before checking whether the nested segment was declared in the related resource’s toRelationships(). Requesting GET /users/1?include=comments.truncate on a read-only endpoint caused Eloquent to invoke truncate() on the Comment query builder, emptying the comments table.
  3. GHSA-mhf7-xp6r-6qrc (Medium) in livewire/livewire (>= 4.3.0, < 4.4.8, fixed in 4.4.8): Livewire 4’s #[Authorize] attribute resolved model arguments by array key ($params[0]) while PHP spread $params positionally (...$params) when invoking the component action or #[On] listener. Sending "params": {"1": 2, "0": 1} authorized model 1 (owned by the caller) and passed model 2 (owned by another user) to the method. Separately, methods carrying both #[Json] and #[Authorize] executed their method body before the authorization check ran.

Upgrade laravel/framework to 12.68.0 or 13.29.0 (or later) and livewire/livewire to 4.4.8 (or later).

Affected packages and projects

EcosystemPackageCVE / GHSASeverityAffected versionsFixed versionVulnerable component
Packagistlaravel/frameworkCVE-2026-114509 (GHSA-f9x3-7vq9-cph6)CVSS v4.0 9.2 Critical< 12.68.0, >= 13.0.0, < 13.29.012.68.0, 13.29.0src/Illuminate/Auth/TokenGuard.php (user() and validate())
Packagistlaravel/frameworkCVE-2026-114508 (GHSA-5wcm-3v2w-8362)CVSS v3.1 6.5 Medium>= 12.45.0, < 12.68.0, >= 13.0.0, < 13.27.012.68.0, 13.27.0src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php
Packagistlivewire/livewireGHSA-mhf7-xp6r-6qrcCVSS v3.1 6.5 Medium>= 4.3.0, < 4.4.84.4.8src/Mechanisms/HandleComponents/HandleComponents.php, SupportEvents.php, SupportJson.php

Technical analysis

1. CVE-2026-114509: TokenGuard non-string credential bypass on MySQL and MariaDB

When a Laravel application configures an API guard using the built-in token driver in config/auth.php ('driver' => 'token', with the default 'hash' => false), TokenGuard::getTokenForRequest() reads api_token from the query string ($request->query('api_token')), the request body ($request->input('api_token')), the Bearer token, or the HTTP Basic password.

Because $request->query() and $request->input() can return an array when the client sends GET /api/user?api_token[]=0 or POST JSON {"api_token": [0]}, $token is not guaranteed to be a string. Before commit 8dbb08bcca (12.x) and commit 9b21ce0a9b (13.x), TokenGuard::user() and TokenGuard::validate() only checked ! empty($token):

--- a/src/Illuminate/Auth/TokenGuard.php
+++ b/src/Illuminate/Auth/TokenGuard.php
@@ -80,7 +80,7 @@ public function user()
 
         $token = $this->getTokenForRequest();
 
-        if (! empty($token)) {
+        if (is_string($token) && ! empty($token)) {
             $user = $this->provider->retrieveByCredentials([
                 $this->storageKey => $this->hash ? hash('sha256', $token) : $token,
             ]);
@@ -110,7 +110,7 @@ public function getTokenForRequest()
      */
     public function validate(array $credentials = [])
     {
-        if (empty($credentials[$this->inputKey])) {
+        if (! is_string($credentials[$this->inputKey] ?? null) || empty($credentials[$this->inputKey])) {
             return false;
         }

When $this->hash is false and $token is [0], TokenGuard passes ['api_token' => [0]] to EloquentUserProvider::retrieveByCredentials(). Inside retrieveByCredentials(), $query->where('api_token', [0]) flattens the single-element array into the first positional binding of SELECT * FROM users WHERE api_token = ? LIMIT 1 with bound integer 0.

In MySQL and MariaDB, comparing a VARCHAR or CHAR column (api_token) against an integer literal 0 converts the stored string to a floating-point number from left to right. Any stored token that begins with a non-digit character (a..z, A..Z) evaluates to 0 ('a8f5f167...' = 0 is TRUE), so the query returns the first matching user row in the table and authenticates the request as that user. Note that if 'hash' => true is enabled in PHP 8+, calling hash('sha256', [0]) throws a TypeError instead of authenticating, which is why the advisory specifically scopes the bypass to unhashed TokenGuard setups (or custom providers that accept array credentials).

Laravel’s JsonApiResource (Illuminate\Http\Resources\JsonApi\JsonApiResource) supports sparse relationship inclusion through the ?include= query parameter (for example, GET /users/1?include=posts,comments).

In laravel/framework 12.45.0 through 12.67.x and 13.0.0 through 13.26.x, compileResourceRelationships() in src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php validated the top-level relationship name against $this->toRelationships($request), resolved $relatedModels, and then immediately passed any dot-prefixed child segments ($request->sparseIncluded($relationName)) to $relatedModels->loadMissing($relations):

// src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php (vulnerable)
foreach ($resourceRelationships as $relationName => $relationResolver) {
    $relatedModels = $relationResolver->handle($this->resource);
    $relatedResourceClass = $relationResolver->resourceClass();

    if (! is_null($relatedModels) && $this->includesPreviouslyLoadedRelationships === false) {
        if (! empty($relations = $request->sparseIncluded($relationName))) {
            $relatedModels->loadMissing($relations);
        }
    }

While the top-level $relationName (comments) was restricted to the allowlist returned by toRelationships(), the nested segment after the dot (truncate in ?include=comments.truncate or posts.comments.truncate) was never checked against the child JsonApiResource’s toRelationships() map before loadMissing(['truncate']) ran on the Eloquent collection. When Eloquent eager-loads a relation name on a model, it calls $model->{$name}() and invokes constraints on the returned object; because Eloquent models forward unknown methods to a new Eloquent\Builder instance via __call(), calling $comment->truncate() executes TRUNCATE TABLE comments directly against the database during a GET request.

Commit 50a7970efe (PR #61323 for 12.68.0, and commit b89f294499 / PR #61322 for 13.27.0) removes the unvalidated $relatedModels->loadMissing($relations) call and instead passes $requestedRelationships down to each child JsonApiResource, which filters every level against its own declared toRelationships($request) map.

3. GHSA-mhf7-xp6r-6qrc: Livewire #[Authorize] parameter key confusion and #[Json] ordering

Livewire 4 (>= 4.3.0, < 4.4.8) introduced the #[Authorize('ability', 'modelParam')] method attribute to run Laravel Gate policy checks before executing component actions. Commit 822a4fa9b1 fixes three flaws in how that attribute interacted with Livewire’s wire protocol:

  1. Out-of-order integer keys on component actions (HandleComponents.php): When #[Authorize('delete', 'post')] inspected the incoming params array for the first parameter (index 0), it looked up $params[0] by array key. When Livewire later invoked deletePost(Post $post), PHP spread $params in iteration order ($component->{$method}(...$params)). In PHP, an associative JSON object "params": {"1": 2, "0": 1} preserves insertion order (2 first, 1 second) while $params[0] evaluates to 1. A user who owned post 1 could therefore pass the Gate check on post 1 while deletePost() received post 2 as its first positional argument.
  2. The same key-order mismatch on #[On] event listeners (SupportEvents.php): Calling __dispatch with ["delete-post", {"1": 2, "0": 1}] triggered the identical mismatch on methods decorated with both #[On] and #[Authorize].
  3. #[Json] methods executed before #[Authorize] (SupportJson.php): Livewire’s SupportJson call hook intercepted methods marked with #[Json], ran the method body immediately, and called $returnEarly(), before the regular BaseAuthorize method-level attribute hook had a chance to run. Even though the response later returned HTTP 403, any database write or deletion inside the #[Json] method had already committed.

Livewire 4.4.8 adds Utils::hasUnambiguousParameterKeys() to reject any params array that is neither a sequential list (array_is_list($params)) nor an all-string-keyed map, and runs all method-level BaseAuthorize attributes at the top of SupportJson’s call listener:

// src/Drawer/Utils.php in livewire/livewire 4.4.8
static function hasUnambiguousParameterKeys(array $params)
{
    return array_is_list($params) || array_filter(array_keys($params), 'is_int') === [];
}

Detection and remediation

1. Audit installed versions and search for affected patterns

Run the following commands across your PHP repositories and container images:

# Check installed versions in composer.lock
composer show laravel/framework livewire/livewire 2>/dev/null || true

rg -n -A 3 '"name":\s*"(laravel/framework|livewire/livewire)"' composer.lock 2>/dev/null

# 1. Check config/auth.php for guards using the 'token' driver without 'hash' => true (CVE-2026-114509)
rg -n "'driver'\s*=>\s*'token'" config/ 2>/dev/null

# 2. Check for JsonApiResource usage with toRelationships() (CVE-2026-114508)
rg -n 'extends\s+JsonApiResource|use\s+Illuminate\\Http\\Resources\\JsonApi\\JsonApiResource|toRelationships\s*\(' app/ src/ 2>/dev/null

# 3. Check for Livewire 4 #[Authorize] attributes (GHSA-mhf7-xp6r-6qrc)
rg -n '#\[Authorize\b' app/ src/ 2>/dev/null

2. Upgrade packages and apply defense-in-depth

  1. Upgrade laravel/framework and livewire/livewire:
    composer update laravel/framework livewire/livewire --with-dependencies
    Ensure laravel/framework resolves to >= 12.68.0 (on 12.x) or >= 13.29.0 (on 13.x), and livewire/livewire resolves to >= 4.4.8.
  2. Migrate legacy TokenGuard endpoints to hashed tokens or Laravel Sanctum: Even on patched Laravel versions, storing plaintext API tokens in the database ('hash' => false) exposes every user if the users table is read. Enable 'hash' => true in config/auth.php or migrate token authentication to Laravel Sanctum.
  3. If you cannot upgrade Livewire 4 immediately: Call $this->authorize('delete', $model) directly inside the component method body rather than relying on the #[Authorize] attribute until v4.4.8 is deployed.

Timeline

  • 2026-10-13: Laravel merges JsonApiResource relationship-inclusion fixes in PRs #61322 (b89f294499) and #61323 (50a7970efe) and ships v12.68.0 and v13.27.0.
  • 2026-10-15: Laravel commits the TokenGuard is_string() check (8dbb08bcca / 9b21ce0a9b) and ships v13.29.0 (backported to v12.68.0).
  • 2026-10-21: Livewire merges commit 822a4fa9b1 and releases livewire/livewire v4.4.8; NVD publishes CVE-2026-114508 and CVE-2026-114509.
  • 2026-10-23 (20:51 to 21:50 UTC): GitHub publishes the reviewed advisories GHSA-mhf7-xp6r-6qrc, GHSA-5wcm-3v2w-8362 (CVE-2026-114508), and GHSA-f9x3-7vq9-cph6 (CVE-2026-114509, rated CVSS v4.0 9.2 Critical).

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References