
Prova di concetto e analisi tecnica per CVE-2026-65971 — SQL injection tramite la proprietà Livewire sortDirection in power-components/livewire-powergrid (< 6.10.4)
sortDirectionProva di concetto e descrizione tecnica completa per CVE-2026-65971 / GHSA-7fgc-3h6c-698r,
un'iniezione SQL in power-components/livewire-powergrid
raggiungibile tramite la proprietà pubblica di Livewire sortDirection.
| CVE | CVE-2026-65971 |
| GHSA | GHSA-7fgc-3h6c-698r |
| Pacchetto | power-components/livewire-powergrid (Composer / Packagist) |
| Versioni interessate | >= 6.0.0, < 6.10.4 |
| Versione corretta | 6.10.4 |
| Debolezza | CWE-89 — Neutralizzazione impropria di elementi speciali usati in un comando SQL |
| Gravità | 7.6 Alta — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L |
| Segnalata da | Caio Fabrício (@BiiTts) |
| Divulgazione | Coordinata, tramite advisory di sicurezza privato su 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 |
---
## 🧠 Riepilogo
PowerGrid è un componente datatable per Laravel + Livewire (~2k stelle, ampiamente utilizzato nei
pannelli di amministrazione Laravel). Il suo stato di ordinamento risiede in due **proprietà Livewire pubbliche**:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
In Livewire, una proprietà pubblica fa parte del formato wire del componente — qualsiasi client che possa
raggiungere il componente può impostarla tramite POST /livewire/update. Questo è voluto; il confine di
sicurezza è ciò che il server fa con il valore.
La funzionalità naturalSort() di PowerGrid costruisce un'espressione ORDER BY grezza contenente il
segnaposto letterale {sortDirection}, e una pipeline sostituisce quel segnaposto con il valore della
proprietà grezzo, non validato prima di passare la stringa a orderByRaw(). La parola chiave della
direzione finisce quindi letteralmente dentro SQL.
Il orderBy() di Laravel stesso rifiuta qualsiasi cosa che non sia asc/desc, ed è quella validazione a
rendere sicuro il normale percorso di ordinamento. Il bug è che esiste un secondo percorso non validato
verso la stessa clausola — e un attaccante può raggiungerlo saltando del tutto quello validante
(vedi The bypass).
Risultato: SQL arbitrario nella clausola ORDER BY, sfruttabile come oracolo cieco booleano/temporale
per leggere qualsiasi dato che l'utente del database può leggere.
Chiunque possa raggiungere una tabella PowerGrid che usa naturalSort può leggere dati arbitrari dal
database — altre tabelle, hash di password, token di sessione, chiavi API, record cross-tenant —
usando un oracolo temporale / booleano.
SELECT.; UPDATE ... non viene eseguito. L'impatto in scrittura è limitato a ciò che una sottoquery può attivare.SLEEP() e sottoquery pesanti —
facilmente abusabili per bloccare i thread del database.Privilegi richiesti: PR:L perché una datatable si trova normalmente dietro l'autenticazione
dell'applicazione. Se la tabella interessata viene renderizzata su una pagina non autenticata, si ricalcola
con PR:N → 8.2 Alto.
Tre file, tre stadi. Tutti i riferimenti sono alla tag vulnerabile v6.10.3.
`src/Concerns/Sorting.php````php public string $sortField = 'id'; // line 11 public string $sortDirection = 'asc'; // line 13
Nessuna delle due proprietà ha una whitelist, una regola di validazione o un setter di normalizzazione. `sortDirection` viene solo assegnato o invertito:```php
public function reverseSort(): string // line 37
{
return $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
updatedSortDirection() (riga 103) esiste — il punto naturale per la validazione — ma in v6.10.3
gestisce solo la contabilità del lazy loading. Non ispeziona mai il valore.
Poiché Livewire idrata le proprietà pubbliche direttamente dalla richiesta, sortDirection è
completamente controllabile dall'attaccante, come stringa arbitraria, a questo punto.
naturalSort() inserisce un segnapostosrc/Providers/Macros.php, righe 102–116 — la macro per 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()` si risolve in un'espressione per driver costruita da
`getSortSqlByDriver()` in `src/DataSource/Support/Sql.php` (righe 60–100). Ogni variante driver
termina con lo stesso segnaposto letterale:```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 vulnerabilità è indipendente dal driver — ogni ramo interpola {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);
}
e l'esecuzione, riga 52:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') restituisce la stringa dell'attaccante, preg_replace_callback
la inserisce nel testo SQL e orderByRaw() — che per contratto non esegue l'escape del suo
argomento — la passa al database.