
Preuve de concept et analyse technique pour CVE-2026-65971 — injection SQL via la propriété Livewire sortDirection dans power-components/livewire-powergrid (< 6.10.4)
sortDirectionPreuve de concept et analyse technique complète pour CVE-2026-65971 / GHSA-7fgc-3h6c-698r,
une injection SQL dans power-components/livewire-powergrid
accessible via la propriété Livewire publique sortDirection.
| CVE | CVE-2026-65971 |
| GHSA | GHSA-7fgc-3h6c-698r |
| Paquet | power-components/livewire-powergrid (Composer / Packagist) |
| Versions affectées | >= 6.0.0, < 6.10.4 |
| Version corrigée | 6.10.4 |
| Faiblesse | CWE-89 — Neutralisation incorrecte des éléments spéciaux utilisés dans une commande SQL |
| Sévérité | 7.6 Élevée — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L |
| Signalé par | Caio Fabrício (@BiiTts) |
| Divulgation | Coordonnée, via un avis de sécurité privé 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 |
---
## 🧠 Résumé
PowerGrid est un composant de datatable pour Laravel + Livewire (~2k étoiles, largement utilisé dans
les panneaux d'administration Laravel). Son état de tri réside dans deux **propriétés Livewire publiques**:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
Dans Livewire, une propriété publique fait partie du format wire du composant — tout client qui peut atteindre le composant peut la définir via POST /livewire/update. C'est voulu ; la frontière de sécurité réside dans ce que le serveur fait de la valeur.
La fonctionnalité naturalSort() de PowerGrid construit une expression ORDER BY brute contenant le littéral placeholder {sortDirection}, et un pipeline substitue ce placeholder par la valeur de propriété brute, non validée avant de passer la chaîne à orderByRaw(). Le mot-clé de direction se retrouve donc tel quel dans le SQL.
La méthode orderBy() de Laravel rejette tout ce qui n'est pas asc/desc, et c'est cette validation qui rend le chemin de tri ordinaire sûr. Le bug est qu'un second chemin, non validé, mène à la même clause — et qu'un attaquant peut l'emprunter en contournant entièrement le chemin validant (voir Le contournement).
Résultat : du SQL arbitraire dans la clause ORDER BY, exploitable comme oracle aveugle booléen/temporel pour lire toutes les données que l'utilisateur de la base peut lire.
Toute personne pouvant atteindre une table PowerGrid qui utilise naturalSort peut lire des données arbitraires depuis la base de données — autres tables, hachages de mots de passe, jetons de session, clés API, enregistrements multi-tenant — en utilisant un oracle temporel / booléen.
SELECT.; UPDATE ... ne s'exécute pas. L'impact en écriture se limite à ce qu'une sous-requête peut déclencher.SLEEP() et des sous-requêtes lourdes — trivialement abusables pour bloquer les threads de la base.Les privilèges requis sont PR:L car une datatable se trouve normalement derrière l'authentification de l'application. Si la table affectée est rendue sur une page non authentifiée, recalculez avec PR:N → 8.2 Élevé.
Trois fichiers, trois étapes. Toutes les références concernent le tag vulnérable v6.10.3.
`src/Concerns/Sorting.php````php public string $sortField = 'id'; // line 11 public string $sortDirection = 'asc'; // line 13
Aucune des deux propriétés n'a de liste blanche, de règle de validation ou de setter de normalisation. `sortDirection` est uniquement affectée ou inversée :```php
public function reverseSort(): string // line 37
{
return $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
updatedSortDirection() (ligne 103) existe — l'endroit naturel pour la validation — mais dans v6.10.3
il ne gère que la comptabilité du chargement paresseux. Il n'inspecte jamais la valeur.
Parce que Livewire hydrate les propriétés publiques directement à partir de la requête, sortDirection est
totalement contrôlée par l'attaquant, comme une chaîne arbitraire, à ce stade.
naturalSort() insère un espace réservésrc/Providers/Macros.php, lignes 102–116 — la macro de colonne 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 résout en une expression propre à chaque pilote construite par
`getSortSqlByDriver()` dans `src/DataSource/Support/Sql.php` (lignes 60–100). Chaque variante de pilote
se termine par le même placeholder littéral :```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
La vulnérabilité est indépendante du driver — chaque branche interpole {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);
}
et l'exécution, ligne 52:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') renvoie la chaîne de l'attaquant, preg_replace_callback
l'insère dans le texte SQL, et orderByRaw() — qui, par contrat, n'échappe pas son
argument — la transmet à la base de données.