
Доказательство концепции и техническое описание для CVE-2026-65971 — SQL-инъекция через свойство sortDirection Livewire в power-components/livewire-powergrid (< 6.10.4)
sortDirectionКонцептуальное доказательство и полное техническое описание для CVE-2026-65971 / GHSA-7fgc-3h6c-698r,
SQL-инъекция в power-components/livewire-powergrid
доступная через публичное свойство Livewire sortDirection.
| CVE | CVE-2026-65971 |
| GHSA | GHSA-7fgc-3h6c-698r |
| Пакет | power-components/livewire-powergrid (Composer / Packagist) |
| Затронутые версии | >= 6.0.0, < 6.10.4 |
| Исправлено | 6.10.4 |
| Уязвимость | CWE-89 — Неправильная нейтрализация специальных элементов, используемых в SQL-команде |
| Серьёзность | 7.6 Высокая — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L |
| Сообщил | Caio Fabrício (@BiiTts) |
| Раскрытие | Координированное, через частное уведомление о безопасности 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 |
---
## 🧠 Краткое описание
PowerGrid — это компонент таблицы данных (datatable) для Laravel + Livewire (~2k звезд, широко используется в административных панелях Laravel). Его состояние сортировки хранится в двух **public свойствах Livewire**:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
В Livewire публичное свойство является частью wire-формата компонента — любой клиент, который может добраться до компонента, может установить его через POST /livewire/update. Это сделано намеренно; граница безопасности заключается в том, что сервер делает с этим значением.
Функция naturalSort() PowerGrid строит сырое выражение ORDER BY, содержащее литеральный заполнитель {sortDirection}, и конвейер заменяет этот заполнитель на сырое, непроверенное значение свойства перед передачей строки в orderByRaw(). Таким образом, ключевое слово направления попадает в SQL дословно.
Собственный orderBy() Laravel отклоняет всё, что не является asc/desc, и эта проверка делает обычный путь сортировки безопасным. Ошибка в том, что существует второй, непроверенный путь к тому же предложению — и злоумышленник может добраться до него, полностью минуя проверяющий путь (см. Обход).
Результат: произвольный SQL в предложении ORDER BY, эксплуатируемый как слепой булевый/временной оракул для чтения любых данных, которые может прочитать пользователь базы данных.
Любой, кто может получить доступ к таблице PowerGrid, использующей naturalSort, может прочитать произвольные данные из базы данных — другие таблицы, хеши паролей, токены сессий, ключи API, записи других арендаторов — с помощью временного/булевого оракула.
SELECT.; UPDATE ... не выполняется. Влияние на запись ограничено тем, что может вызвать подзапрос.SLEEP() и тяжелые подзапросы — тривиально злоупотребляемые для блокировки потоков базы данных.Требуемые привилегии — PR:L, потому что таблица данных обычно находится за аутентификацией приложения. Если затронутая таблица отображается на неаутентифицированной странице, пересчитайте с PR:N → 8.2 Высокий.
Три файла, три стадии. Все ссылки относятся к уязвимому тегу v6.10.3.
`src/Concerns/Sorting.php````php public string $sortField = 'id'; // line 11 public string $sortDirection = 'asc'; // line 13
Ни одно из свойств не имеет белого списка, правила проверки или нормализующего сеттера. `sortDirection` только присваивается или переворачивается:```php
public function reverseSort(): string // line 37
{
return $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
updatedSortDirection() (строка 103) существует — естественное место для проверки, — но в v6.10.3
он обрабатывает только бухгалтерию ленивой загрузки. Он никогда не проверяет значение.
Поскольку Livewire гидратирует публичные свойства напрямую из запроса, sortDirection на этом этапе
полностью контролируется атакующим как произвольная строка.
naturalSort() помещает заполнительsrc/Providers/Macros.php, строки 102–116 — макрос столбца 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()` разрешается в выражение, зависящее от драйвера, создаваемое с помощью `getSortSqlByDriver()` в `src/DataSource/Support/Sql.php` (строки 60–100). Все варианты драйверов заканчиваются одним и тем же буквальным заполнителем:```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
Уязвимость является независимой от драйвера — каждая ветвь интерполирует {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);
}
и выполнение, line 52:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') возвращает строку атакующего, preg_replace_callback
вставляет её в SQL-текст, а orderByRaw() — который по контракту не экранирует свой
аргумент — передаёт её базе данных.