Neutron, nuestro motor de IA, obtuvo un 97.1% en el benchmark CyberGym de UC Berkeley. Más información

Análisis a fondo

Análisis a fondo

Omisión de autenticación en Token Guard de Laravel (CVE-2026-114509)

Laravel califica CVE-2026-114509 como crítico, CVSS v4.0 9.2: un atacante no autenticado puede iniciar sesión como un usuario existente del guard de API heredado token en MySQL o MariaDB enviando un cuerpo JSON como {"api_token": [0]}, sin conocer ningún token de API válido. Corregido en 12.68.0 y 13.29.0, con la corrección también presente en la etiqueta 11.56.1. Existe una prueba de concepto pública desde el 22 de octubre de 2026 y no se conoce explotación en ataques reales.

Resumen

  • El guard de tokens integrado de Laravel, el controlador de autenticación token, autentica una solicitud buscando el valor api_token en la tabla de usuarios. Antes de la corrección, el guard nunca comprobaba que el token fuera una cadena, de modo que un cuerpo JSON manipulado convertía la búsqueda en una consulta WHERE api_token IN (...) con valores elegidos por el atacante.
  • En MySQL y MariaDB, un cuerpo como {"api_token": [0]} hace que la base de datos convierta cada token de texto almacenado en un número, por lo que cualquier token que empiece por una letra se considera igual a 0. La consulta devuelve ese usuario y la solicitud se autentica con su identidad, sin token válido ni fuerza bruta.
  • Laravel califica el fallo como crítico, CVSS v4.0 9.2, y el 22 de octubre de 2026 se publicó una prueba de concepto pública. El fallo no figura en el catálogo CISA KEV y no se conoce explotación en ataques reales a fecha del 24 de octubre de 2026.
  • Actualice ahora a 12.68.0, 13.29.0 o la etiqueta 11.56.1, o añada un middleware que rechace valores api_token que no sean cadenas. La corrección se publicó el 25 de agosto de 2026, por lo que una actualización de dependencias posterior podría haberle protegido ya.

¿Estoy afectado?

Está afectado si se cumplen todas estas condiciones:

  • config/auth.php define un guard con 'driver' => 'token' y un endpoint lo utiliza (rutas con auth:api o auth:<guard> cuando ese guard es el de tokens).
  • La versión del framework es anterior a la corregida para su rama: anterior a 12.68.0 en Laravel 12, a 13.29.0 en Laravel 13, a la etiqueta 11.56.1 en Laravel 11, o cualquier rama fuera de soporte (10.x y anteriores).
  • La tabla de usuarios reside en MySQL o MariaDB. PostgreSQL rechaza la comparación de tipos incompatibles y SQLite no devuelve filas, como demuestra la PoC pública.
  • El guard no establece 'hash' => true en PHP 8. El creador de guards de Laravel utiliza false para hash cuando falta la clave, y las configuraciones iniciales que incluyen el guard de tokens lo establecen explícitamente en false.

No está afectado si utiliza Laravel Sanctum o Passport, o cualquier guard distinto del controlador token. Sanctum lee un encabezado Bearer, que siempre es una cadena, calcula su hash con SHA-256 y almacena los tokens en una tabla independiente.

Rama Versiones afectadas Versión corregida
13.x >= 13.0.0, < 13.29.0 13.29.0
12.x < 12.68.0 12.68.0
11.x < 11.56.1 etiqueta 11.56.1
10.x y anteriores todas las versiones con el guard de tokens ninguna (fin de soporte)

Compruebe qué se ejecuta en sus despliegues:

# Is the token guard configured at all?
grep -n "'driver' => 'token'" config/auth.php

# Which framework version is installed?
composer show laravel/framework | grep versions

# Which database backend?
grep DB_CONNECTION .env

# Does composer already flag it?
composer audit

En una imagen de contenedor:

docker run --rm <image> php -r "echo shell_exec('composer show laravel/framework 2>/dev/null | grep versions') ?: 'composer not in image';"

El historial del guard importa: las plantillas iniciales de Laravel 6.x y 7.x incluían el guard de tokens por defecto con 'hash' => false, mientras que las de 8.x y 9.x no contienen ningún guard de API. composer update nunca sobrescribe config/auth.php, por lo que una aplicación actualizada desde una plantilla antigua puede conservar silenciosamente el guard de tokens. La PoC pública documenta este historial de configuración y los archivos de las plantillas 6.x y 7.x lo confirman. Packagist registró 19.679.151 descargas de laravel/framework en el último mes (consultado el 24 de octubre de 2026), de modo que son muchas las instalaciones que necesitan esta comprobación.

¿Cómo lo corrijo?

Actualice el framework a la primera versión corregida de su rama:

# Laravel 12
composer require "laravel/framework:^12.68.0" --update-with-dependencies

# Laravel 13
composer require "laravel/framework:^13.29.0" --update-with-dependencies

La corrección también existe en la etiqueta v11.56.1 de laravel/framework, verificado mediante la lectura de src/Illuminate/Auth/TokenGuard.php en esa etiqueta. El aviso solo enumera los intervalos 12.x y 13.x, por lo que conviene confirmar el resultado en el código si fija 11.56.1.

Si no puede actualizar, añada un middleware que rechace tokens que no sean cadenas antes de que lleguen al guard, como recomienda la PoC:

public function handle($request, Closure $next)
{
    $token = $request->input('api_token');
    if ($token !== null && !is_string($token)) {
        abort(400, 'Invalid token format');
    }
    return $next($request);
}

El middleware bloquea esta clase de ataque. Sus límites: debe registrarse en cada ruta o grupo que pueda llegar al guard de tokens y no resuelve la confusión de tipos subyacente. Una regla de firewall de aplicaciones web que rechace valores JSON o de consulta donde api_token sea un array logra lo mismo, y la PoC enumera patrones de consulta y JSON para ello. La solución duradera es actualizar o migrar la API a Laravel Sanctum, que no utiliza esta búsqueda.

¿Cuál es la causa raíz?

El guard de tokens trataba cualquier credencial no vacía como una cadena, y Eloquent convierte una credencial de tipo array en una lista IN. Así, un array JSON se convertía en una comparación SQL con una lista de valores elegidos por el atacante.

La comprobación vulnerable, en src/Illuminate/Auth/TokenGuard.php de v12.67.0, línea 83:

$token = $this->getTokenForRequest();

if (! empty($token)) {
    $user = $this->provider->retrieveByCredentials([
        $this->storageKey => $this->hash ? hash('sha256', $token) : $token,
    ]);
}

empty() devuelve false para un array no vacío, de modo que ! empty([0]) se cumple y el array [0] continúa. Dos hechos más completan la cadena:

  • El creador de guards de src/Illuminate/Auth/AuthManager.php (línea 166) pasa $config['hash'] ?? false, por lo que una configuración sin clave hash conserva el token sin procesar. Con 'hash' => true en PHP 8, hash('sha256', [0]) lanza un TypeError y la solicitud termina con un error en lugar de eludir la autenticación. La PoC señala que PHP 7 solo emite una advertencia y continúa, de modo que las plataformas antiguas siguen expuestas incluso con hashing activado.
  • EloquentUserProvider::retrieveByCredentials (línea 129) trata por diseño las credenciales de tipo array como una lista, para que otros flujos de autenticación puedan buscar varios candidatos:
foreach ($credentials as $key => $value) {
    if (is_array($value) || $value instanceof Arrayable) {
        $query->whereIn($key, $value);
    }

Por tanto, la credencial del guard deja de ser «el token que comparar» y se convierte en «la lista de tokens con los que el usuario puede coincidir». Los commits del parche, ambos titulados «corregir la recuperación del token» y fechados el 24 de agosto de 2026, cierran la confusión de tipos exigiendo una cadena antes de la búsqueda:

-        if (! empty($token)) {
+        if (is_string($token) && ! empty($token)) {

y la misma comprobación en validate(), el segundo punto de entrada que pasa una credencial a retrieveByCredentials sin ningún hashing.

¿Cómo se explota?

El ataque necesita una solicitud con cuerpo JSON, como demuestra la PoC pública:

  1. El atacante envía POST /api/user con Content-Type: application/json y cuerpo {"api_token": [0]}. request->input() de Laravel decodifica el JSON, por lo que la credencial es el array PHP [0], un entero dentro de una lista.
  2. TokenGuard::getTokenForRequest() devuelve [0] desde la entrada de la solicitud y ! empty([0]) se cumple.
  3. Con hash en false, el array sin procesar llega a retrieveByCredentials, que construye WHERE api_token IN (0) porque la credencial es un array.
  4. MySQL y MariaDB comparan la columna de texto con el número 0 convirtiendo cada token almacenado en un número. Un token que empieza por una letra, el caso habitual en los tokens de API, se convierte en 0 y resulta igual.
  5. ->first() devuelve la primera fila coincidente. El laboratorio de la PoC crea cuatro usuarios y la solicitud vuelve autenticada como administrador, el primer usuario creado, sin que se haya enviado ningún token válido.
  6. Cada solicitud posterior de esa sesión se ejecuta con los permisos de ese usuario, de modo que la omisión se convierte en una toma de control de la cuenta en todas las rutas protegidas por el guard.

La solicitud exacta del atacante de la PoC, reproducible contra su laboratorio:

curl -s http://target/api/user \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -X POST -d '{"api_token": [0]}'

Una cadena de consulta como ?api_token[]=0 también entrega un array, pero de la cadena "0", que la base de datos compara como texto. Por eso, la ruta que funciona es el cuerpo JSON con un número sin comillas.

Flujo del ataque

flowchart LR A["POST con cuerpo JSON
{"api_token": [0]}"] --> B["getTokenForRequest()
devuelve el array [0]"] B --> C["! empty([0]) se cumple,
sin comprobación is_string"] C --> D["La credencial array
se convierte en WHERE api_token IN (0)"] D --> E["MySQL convierte tokens a números,
los que empiezan por letra pasan a 0"] E --> F["first() devuelve
el primer usuario coincidente"] F --> G["Solicitud autenticada
como ese usuario"]

¿Qué puede hacer un atacante?

El atacante se autentica como un usuario existente del guard de tokens y obtiene todo lo que permita el acceso a la API de ese usuario: lectura, escritura y eliminación mediante rutas de API, y cualquier dato al que pueda acceder esa capa de rutas. Las condiciones y límites, según el aviso y la PoC:

  • No necesita credenciales, interacción del usuario ni una posición de red distinta de poder alcanzar la API.
  • El guard debe ser el controlador token en MySQL o MariaDB con hash en false. Los despliegues PostgreSQL y SQLite no están afectados por la comparación con el entero.
  • El atacante no elige la cuenta. La consulta devuelve la primera fila coincidente, que en el laboratorio de la PoC era el administrador porque ese usuario se creó primero.
  • Los despliegues Sanctum y Passport no están afectados. Sus búsquedas de tokens son distintas.

¿Por qué funciona la corrección?

El parche añade is_string() antes de la comprobación de vacío en ambos puntos de entrada, user() y validate():

-        if (! empty($token)) {
+        if (is_string($token) && ! empty($token)) {
             $user = $this->provider->retrieveByCredentials([
-        if (empty($credentials[$this->inputKey])) {
+        if (! is_string($credentials[$this->inputKey] ?? null) || empty($credentials[$this->inputKey])) {

Ahora un array JSON falla la comprobación de cadena y la autenticación devuelve null, el resultado que el guard siempre pretendió. La corrección cierra por completo este problema a nivel del guard: ninguna credencial que no sea una cadena puede llegar ya a retrieveByCredentials mediante el guard de tokens. No cambia retrieveByCredentials, que sigue convirtiendo credenciales array en listas IN para los proveedores que las utilizan deliberadamente. Por tanto, el código de aplicación que pase arrays a búsquedas de autenticación conserva su propio riesgo.

¿Cómo compruebo si he sufrido una intrusión?

Laravel no publica indicadores de compromiso. La PoC documenta el aspecto de la explotación y los patrones son inequívocos porque los clientes reales envían un token de tipo cadena:

  • Cadenas de consulta en registros de acceso: api_token[]=0 y api_token[0]=0.
  • Cuerpos JSON en registros de aplicación o WAF: "api_token":[0] y "api_token":[false].
  • Anomalías que conviene comprobar con independencia del patrón: respuestas correctas en rutas del guard de tokens desde sesiones sin una emisión de token correspondiente, y cuentas de administrador que actúan desde nuevas direcciones de origen.

Cronología

  • 2026-08-24: Taylor Otwell incorpora la corrección, «corregir la recuperación del token», en ambas ramas con soporte.
  • 2026-08-25: Laravel publica v12.68.0 y v13.29.0 con la corrección.
  • 2026-10-19: Laravel publica el aviso GHSA-f9x3-7vq9-cph6, calificado como crítico, CVSS v4.0 9.2, y reconoce a KIKOmanasijev.
  • 2026-10-21: La CNA de GitHub publica el registro de CVE-2026-114509.
  • 2026-10-22: Se publica en GitHub una prueba de concepto pública con un laboratorio Docker.
  • 2026-10-23: GitHub Advisory Database publica y revisa el aviso, añadiendo CWE-287 y CWE-1287.
  • 2026-10-24: Este artículo.

Preguntas frecuentes

¿Están afectados Laravel Sanctum o Passport?

No. Sanctum lee el encabezado Bearer, que siempre es una cadena, y compara un hash SHA-256 con una tabla independiente de tokens de acceso personal, de modo que el array nunca llega a una búsqueda. Passport autentica mediante flujos OAuth2 con almacenamiento y búsqueda diferentes. El aviso solo cubre el guard de tokens integrado.

¿Por qué el ataque solo funciona en MySQL o MariaDB?

La credencial array termina comparándose como el número 0, y MySQL y MariaDB convierten la columna de texto en un número para esa comparación, lo que transforma la mayoría de los tokens reales en 0. PostgreSQL genera un error de tipo para la misma consulta y la afinidad de tipos de SQLite no devuelve filas; ambos casos se demuestran en el laboratorio de la PoC. El error del guard existe en todas partes; la comparación explotable necesita la conversión implícita.

¿Me protege 'hash' => true?

En PHP 8 convierte el ataque en un error: hash('sha256', [0]) lanza un TypeError y la solicitud falla en lugar de autenticar a alguien. En PHP 7, la función hash solo emite una advertencia y devuelve null, por lo que la PoC considera esa combinación todavía expuesta. Si confía en el hashing como única defensa, actualice igualmente y recuerde que las configuraciones iniciales de 6.x y 7.x incluían el guard de tokens con 'hash' => false.

¿Puede el atacante elegir una cuenta concreta?

No directamente. La búsqueda devuelve la primera fila que la base de datos considera coincidente, por lo que el atacante obtiene el primer usuario coincidente; en el laboratorio de la PoC era el primero creado, a menudo un administrador. El atacante también puede enviar varios números en el array JSON para ampliar las coincidencias de conversión, pero la selección sigue siendo la primera que devuelva la base de datos.

Mi aplicación utiliza una versión de Laravel fuera de soporte. ¿Qué hago?

Añada el middleware que rechaza valores api_token que no sean cadenas, o una regla WAF con los patrones anteriores, y planifique una migración a Sanctum, la vía recomendada por la PoC. Las ramas fuera de soporte no recibirán la corrección, de modo que el guard seguirá siendo vulnerable mientras permanezca configurado.

Escanee sus aplicaciones web con Ostorlab para identificar las versiones del framework Laravel y las configuraciones del guard de tokens que exponen, incluidos los despliegues heredados que siguen configurados con el controlador token.

Referencias

  • Aviso de Laravel GHSA-f9x3-7vq9-cph6, publicado el 19 de octubre de 2026: https://github.com/laravel/framework/security/advisories/GHSA-f9x3-7vq9-cph6 (consultado el 24 de octubre de 2026)
  • Registro CVE-2026-114509 con el vector CVSS v4.0 9.2: https://cveawg.mitre.org/api/cve/CVE-2026-114509 (consultado el 24 de octubre de 2026)
  • Entrada de GitHub Advisory Database con la clasificación CWE-287 y CWE-1287: https://github.com/advisories/GHSA-f9x3-7vq9-cph6 (consultado el 24 de octubre de 2026)
  • Commit de corrección en la rama 12.x, fechado el 24 de agosto de 2026: https://github.com/laravel/framework/commit/9b21ce0a9bb2b2599978bc0b0df4abea952ad31c (consultado el 24 de octubre de 2026)
  • Commit de corrección en la rama 13.x, fechado el 24 de agosto de 2026: https://github.com/laravel/framework/commit/8dbb08bccac1be095dba4a9d259a419620f1e8a5 (consultado el 24 de octubre de 2026)
  • Versión v12.68.0, publicada el 25 de agosto de 2026: https://github.com/laravel/framework/releases/tag/v12.68.0 (consultado el 24 de octubre de 2026)
  • Versión v13.29.0, publicada el 25 de agosto de 2026: https://github.com/laravel/framework/releases/tag/v13.29.0 (consultado el 24 de octubre de 2026)
  • Código corregido de TokenGuard en la etiqueta v11.56.1: https://raw.githubusercontent.com/laravel/framework/v11.56.1/src/Illuminate/Auth/TokenGuard.php (consultado el 24 de octubre de 2026)
  • Código vulnerable de TokenGuard en v12.67.0: https://raw.githubusercontent.com/laravel/framework/v12.67.0/src/Illuminate/Auth/TokenGuard.php (consultado el 24 de octubre de 2026)
  • Prueba de concepto pública con laboratorio Docker, publicada el 22 de octubre de 2026: https://github.com/tajulasri/-CVE-2026-114509-POC (consultado el 24 de octubre de 2026)
  • Configuración de autenticación de la plantilla Laravel 7.x con el guard de tokens: https://raw.githubusercontent.com/laravel/laravel/7.x/config/auth.php (consultado el 24 de octubre de 2026)
  • Configuración de autenticación de la plantilla Laravel 6.x con el guard de tokens: https://raw.githubusercontent.com/laravel/laravel/6.x/config/auth.php (consultado el 24 de octubre de 2026)
  • Estadísticas de descargas de laravel/framework en Packagist: https://packagist.org/packages/laravel/framework.json (consultado el 24 de octubre de 2026)