
Proof-of-Concept und technische Beschreibung für CVE-2026-65971 — SQL-Injection über die sortDirection Livewire-Eigenschaft in power-components/livewire-powergrid (< 6.10.4)
sortDirectionProof-of-Concept und vollständige technische Beschreibung für CVE-2026-65971 / GHSA-7fgc-3h6c-698r,
eine SQL-Injection in power-components/livewire-powergrid,
die über die öffentliche Livewire-Eigenschaft sortDirection erreichbar ist.
| CVE | CVE-2026-65971 |
| GHSA | GHSA-7fgc-3h6c-698r |
| Paket | power-components/livewire-powergrid (Composer / Packagist) |
| Betroffen | >= 6.0.0, < 6.10.4 |
| Behoben | 6.10.4 |
| Schwachstelle | CWE-89 — Unzureichende Neutralisierung spezieller Elemente in einem SQL-Befehl |
| Schweregrad | 7.6 Hoch — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L |
| Gemeldet von | Caio Fabrício (@BiiTts) |
| Offenlegung | Koordiniert, über privates GitHub-Sicherheitsadvisory |
| ├── 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 |
---
## 🧠 Zusammenfassung
PowerGrid ist eine Datatable-Komponente für Laravel + Livewire (~2k Sterne, weit verbreitet in
Laravel-Admin-Panels). Sein Sortierzustand lebt in zwei **öffentlichen Livewire-Eigenschaften**:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
In Livewire ist eine öffentliche Eigenschaft Teil des Wire-Formats der Komponente – jeder Client, der die Komponente erreichen kann, kann sie über POST /livewire/update setzen. Das ist beabsichtigt; die Sicherheitsgrenze liegt darin, was der Server mit dem Wert tut.
PowerGrids naturalSort()-Funktion erstellt einen rohen ORDER BY-Ausdruck, der den Platzhalter {sortDirection} enthält, und eine Pipeline ersetzt diesen Platzhalter mit dem rohen, unvalidierten Eigenschaftswert, bevor die Zeichenkette an orderByRaw() übergeben wird. Das Richtungsschlüsselwort landet daher wörtlich in SQL.
Laravels eigene orderBy()-Methode lehnt alles ab, was nicht asc/desc ist, und diese Validierung macht den normalen Sortierpfad sicher. Der Fehler besteht darin, dass ein zweiter, unvalidierter Pfad zur selben Klausel existiert – und ein Angreifer ihn erreichen kann, während er den validierenden vollständig umgeht (siehe The bypass).
Ergebnis: Beliebiges SQL in der ORDER BY-Klausel, ausnutzbar als blindes boolesches/zeitbasiertes Orakel, um alle Daten zu lesen, die der Datenbankbenutzer lesen kann.
Jeder, der eine PowerGrid-Tabelle erreichen kann, die naturalSort verwendet, kann beliebige Daten aus der Datenbank lesen – andere Tabellen, Passwort-Hashes, Sitzungstoken, API-Schlüssel, mandantenübergreifende Datensätze – mithilfe eines zeit-/booleschen Orakels.
SELECT kann.; UPDATE ... nicht ausgeführt wird. Schreibauswirkungen sind auf das beschränkt, was eine Unterabfrage auslösen kann.SLEEP() und schwere Unterabfragen – trivial missbrauchbar, um Datenbank-Threads zu blockieren.Erforderliche Berechtigungen sind PR:L, da eine Datentabelle normalerweise hinter der Anwendungsauthentifizierung sitzt. Wenn die betroffene Tabelle auf einer nicht authentifizierten Seite gerendert wird, neu berechnen mit PR:N → 8.2 Hoch.
Drei Dateien, drei Stufen. Alle Referenzen beziehen sich auf den anfälligen Tag v6.10.3.
`src/Concerns/Sorting.php````php public string $sortField = 'id'; // line 11 public string $sortDirection = 'asc'; // line 13
Keine der Eigenschaften hat eine Whitelist, eine Validierungsregel oder einen normalisierenden Setter. `sortDirection` wird nur zugewiesen oder umgeschaltet:```php
public function reverseSort(): string // line 37
{
return $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
updatedSortDirection() (Zeile 103) existiert — der natürliche Ort für Validierung —, aber in v6.10.3
verarbeitet es nur Lazy-Loading-Buchhaltung. Es überprüft den Wert nie.
Da Livewire öffentliche Eigenschaften direkt aus der Anfrage füllt, ist sortDirection an dieser Stelle
vollständig angreiferkontrolliert, als beliebiger String.
naturalSort() setzt einen Platzhaltersrc/Providers/Macros.php, Zeilen 102–116 — das naturalSort-Spalten-Makro:```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()` ergibt einen treiberspezifischen Ausdruck, der von
`getSortSqlByDriver()` in `src/DataSource/Support/Sql.php` (Zeilen 60–100) erstellt wird. Jede Treibervariante
endet mit dem gleichen Literal-Platzhalter:```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
Die Schwachstelle ist treiberunabhängig — jeder Zweig interpoliert {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);
}
und die Ausführung, Zeile 52:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') gibt den String des Angreifers zurück, preg_replace_callback
fügt es in den SQL-Text ein, und orderByRaw() — das vertragsgemäß sein Argument nicht escaped — übergibt es an die Datenbank.