
Prueba de concepto y análisis técnico para CVE-2026-65971 — inyección SQL mediante la propiedad sortDirection de Livewire en power-components/livewire-powergrid (< 6.10.4)
sortDirectionPrueba de concepto e informe técnico completo para CVE-2026-65971 / GHSA-7fgc-3h6c-698r,
una inyección SQL en power-components/livewire-powergrid
accesible a través de la propiedad pública sortDirection de Livewire.
| CVE | CVE-2026-65971 |
| GHSA | GHSA-7fgc-3h6c-698r |
| Paquete | power-components/livewire-powergrid (Composer / Packagist) |
| Afectado | >= 6.0.0, < 6.10.4 |
| Corregido | 6.10.4 |
| Debilidad | CWE-89 — Neutralización incorrecta de elementos especiales utilizados en un comando SQL |
| Gravedad | 7.6 Alta — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L |
| Reportado por | Caio Fabrício (@BiiTts) |
| Divulgación | Coordinada, mediante aviso de seguridad privado de GitHub |
| ├── poc/exploit_powergrid_sqli.py working exploit — confirm + blind extraction | |
| ├── lab/ build the vulnerable app to reproduce it yourself | |
| ├── evidence/EVIDENCE.txt raw lab notes from confirmation | |
| ├── patch/security-fix-v6.10.4.diff the security-relevant portion of the official fix | |
| └── detection/ Sigma rules + Nuclei template for defenders |
---
## 🧠 Resumen
PowerGrid es un componente de datatable para Laravel + Livewire (~2k estrellas, ampliamente utilizado en
paneles de administración de Laravel). Su estado de ordenamiento reside en dos **propiedades públicas de Livewire**:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
En Livewire, una propiedad pública forma parte del formato wire del componente: cualquier cliente que pueda
alcanzar el componente puede establecerla mediante POST /livewire/update. Eso es por diseño; la frontera
de seguridad es lo que el servidor hace con el valor.
La función naturalSort() de PowerGrid construye una expresión ORDER BY cruda que contiene el
placeholder literal {sortDirection}, y un pipeline sustituye ese placeholder por el valor de la
propiedad sin validar antes de pasar la cadena a orderByRaw(). Por lo tanto, la palabra clave de
dirección acaba literalmente dentro del SQL.
El propio orderBy() de Laravel rechaza cualquier cosa que no sea asc/desc, y esa validación es lo que
hace seguro el camino de ordenación ordinario. El fallo es que existe un segundo camino sin validar hacia la misma
cláusula — y un atacante puede alcanzarlo saltándose por completo el camino que sí valida
(véase El bypass).
Resultado: SQL arbitrario en la cláusula ORDER BY, explotable como oráculo ciego booleano/basado en tiempo
para leer cualquier dato que el usuario de la base de datos pueda leer.
Cualquiera que pueda acceder a una tabla de PowerGrid que use naturalSort puede leer datos arbitrarios de
la base de datos — otras tablas, hashes de contraseñas, tokens de sesión, claves de API, registros de otros inquilinos —
mediante un oráculo basado en tiempo / booleano.
SELECT.; UPDATE ... no se ejecuta. El impacto de escritura se limita a lo que una subconsulta pueda desencadenar.SLEEP() y subconsultas pesadas —
trivialmente abusable para inmovilizar hilos de la base de datos.Privilegios requeridos: PR:L porque una tabla de datos normalmente está detrás de la autenticación de la
aplicación. Si la tabla afectada se renderiza en una página no autenticada, recalcular con
PR:N → 8.2 Alta.
Tres archivos, tres etapas. Todas las referencias apuntan a la etiqueta vulnerable v6.10.3.
`src/Concerns/Sorting.php````php public string $sortField = 'id'; // line 11 public string $sortDirection = 'asc'; // line 13
Ninguna de las dos propiedades tiene una lista blanca, una regla de validación ni un setter de normalización. `sortDirection` solo se asigna o se invierte:```php
public function reverseSort(): string // line 37
{
return $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
updatedSortDirection() (línea 103) existe — el lugar natural para la validación — pero en v6.10.3
solo maneja la contabilidad de la carga diferida. Nunca inspecciona el valor.
Debido a que Livewire hidrata las propiedades públicas directamente desde la solicitud, sortDirection es
completamente controlable por el atacante, como una cadena arbitraria, en este punto.
naturalSort() inserta un marcador de posiciónsrc/Providers/Macros.php, líneas 102–116 — la macro de columna naturalSort:```php
Column::macro('naturalSort', function (bool $when = false, ?string $tableName = null): Column {
$this->enableSort();
if ($when) {
$this->rawQueries[] = [
'method' => 'orderByRaw', // <-- raw sink
'sql' => Sql::sortStringAsNumber($this->dataField),
'bindings' => [],
];
}
return $this;
});
`Sql::sortStringAsNumber()` se resuelve a una expresión por driver construida por
`getSortSqlByDriver()` en `src/DataSource/Support/Sql.php` (líneas 60–100). Cada variante de driver
termina con el mismo marcador de posición literal:```php
$default = "$sortField+0 {sortDirection}"; // line 76
'8.0.4' => "CAST(NULLIF(REGEXP_REPLACE($sortField, '[[:alpha:]]+', ''), '') AS SIGNED INTEGER) {sortDirection}", // MySQL, line 81
'0' => "CAST($sortField AS INTEGER) {sortDirection}", // SQLite, line 84
'0' => "CAST(NULLIF(REGEXP_REPLACE($sortField, '\D', '', 'g'), '') AS INTEGER) {sortDirection}", // PgSQL, line 87
'0' => "CAST(SUBSTRING(...) AS INT) {sortDirection}", // SQL Server, line 90
The vulnerability is independiente del controlador — every branch interpolates {sortDirection}.
`src/DataSource/Processors/Database/Pipelines/ColumnRawQueries.php````php private function resolvePlaceholders(?string $sql): ?string // line 56 { if (is_null($sql)) { return null; }
return preg_replace_callback('/\{(\w+)\}/', function ($matches) {
$property = trim($matches[1]);
return data_get($this->component, $property, ''); // line 65 — raw property, no escaping
}, $sql);
}
y la ejecución, línea 52:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') devuelve la cadena del atacante, preg_replace_callback
la inserta en el texto SQL, y orderByRaw() — que por contrato no escapa su
argumento — la pasa a la base de datos.