
Prova de conceito e artigo técnico para CVE-2026-65971 — injeção de SQL via a propriedade Livewire sortDirection em power-components/livewire-powergrid (< 6.10.4)
sortDirectionProva de conceito e descrição técnica completa para CVE-2026-65971 / GHSA-7fgc-3h6c-698r,
uma injeção de SQL em power-components/livewire-powergrid
acessível por meio da propriedade pública do Livewire sortDirection.
| CVE | CVE-2026-65971 |
| GHSA | GHSA-7fgc-3h6c-698r |
| Pacote | power-components/livewire-powergrid (Composer / Packagist) |
| Afetado | >= 6.0.0, < 6.10.4 |
| Corrigido | 6.10.4 |
| Fraqueza | CWE-89 — Neutralização inadequada de Elementos Especiais usados em um Comando SQL |
| Severidade | 7.6 Alta — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L |
| Relatado por | Caio Fabrício (@BiiTts) |
| Divulgação | Coordenada, via aviso de segurança privado do 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 |
---
## 🧠 Resumo
PowerGrid é um componente de datatable para Laravel + Livewire (~2 mil estrelas, amplamente utilizado em
painéis administrativos Laravel). Seu estado de ordenação reside em duas **propriedades públicas do Livewire**:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
No Livewire, uma propriedade pública faz parte do formato wire do componente — qualquer cliente que possa
alcançar o componente pode defini-la por meio de POST /livewire/update. Isso é por design; o limite
de segurança é o que o servidor faz com o valor.
O recurso naturalSort() do PowerGrid constrói uma expressão ORDER BY bruta contendo o espaço reservado literal
{sortDirection}, e um pipeline substitui esse espaço reservado pelo valor bruto e não validado da propriedade
antes de entregar a string para orderByRaw(). A palavra-chave de direção, portanto,
chega literalmente dentro do SQL.
O próprio orderBy() do Laravel rejeita qualquer coisa que não seja asc/desc, e essa validação é o que
torna o caminho comum de ordenação seguro. O bug é que existe um segundo caminho não validado para a mesma
cláusula — e um atacante pode alcançá-lo pulando completamente o caminho de validação
(veja O bypass).
Resultado: SQL arbitrário na cláusula ORDER BY, explorável como um oráculo cego baseado em booleano/tempo
para ler qualquer dado que o usuário do banco de dados possa ler.
Qualquer pessoa que possa alcançar uma tabela do PowerGrid que use naturalSort pode ler dados arbitrários do
banco de dados — outras tabelas, hashes de senhas, tokens de sessão, chaves de API, registros cross-tenant —
usando um oráculo baseado em tempo/booleano.
SELECT.; UPDATE ... não é executado. O impacto de escrita é limitado ao que uma subconsulta pode acionar.SLEEP() e subconsultas pesadas —
trivialmente abusáveis para prender threads do banco de dados.Os privilégios necessários são PR:L, porque uma datatable normalmente fica atrás da autenticação da aplicação.
Se a tabela afetada for renderizada em uma página não autenticada, recalcule com
PR:N → 8.2 Alto.
Três arquivos, três estágios. Todas as referências são à tag vulnerável v6.10.3.
`src/Concerns/Sorting.php````php public string $sortField = 'id'; // line 11 public string $sortDirection = 'asc'; // line 13
Nenhuma das propriedades tem uma lista de permissões, uma regra de validação ou um setter de normalização. `sortDirection` é
apenas atribuído ou invertido:```php
public function reverseSort(): string // line 37
{
return $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
updatedSortDirection() (linha 103) existe — o lugar natural para validação — mas em v6.10.3
ela apenas trata da contabilidade de lazy-loading. Nunca inspeciona o valor.
Como o Livewire hidrata propriedades públicas diretamente da requisição, sortDirection é
totalmente controlada pelo atacante, como uma string arbitrária, neste ponto.
naturalSort() insere um espaço reservadosrc/Providers/Macros.php, linhas 102–116 — a macro de coluna 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()` resolve para uma expressão por driver construída por
`getSortSqlByDriver()` em `src/DataSource/Support/Sql.php` (linhas 60–100). Cada variante de driver
termina com o mesmo placeholder 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
A vulnerabilidade é independente do driver — cada 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 a execução, linha 52:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') retorna a string do atacante, preg_replace_callback a insere no texto SQL, e orderByRaw() — que por contrato não escapa seu argumento — a passa para o banco de dados.