
CVE-2026-65971 の概念実証(PoC)および技術解説 — power-components/livewire-powergrid(< 6.10.4)の sortDirection Livewire プロパティを介した SQLインジェクション
sortDirection を介したSQLインジェクションCVE-2026-65971 / GHSA-7fgc-3h6c-698r の概念実証および完全な技術解説。これは power-components/livewire-powergrid におけるSQLインジェクションであり、公開された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 は、Laravel + Livewire 向けの datatable コンポーネントです(約2k スター、Laravel 管理画面で広く使用されています)。そのソート状態は、2つの **public Livewire プロパティ** に保持されています。```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
Livewire では、public プロパティはコンポーネントの wire 形式の一部です。コンポーネントに到達できるクライアントは、POST /livewire/update を通じてその値を設定できます。これは設計によるものであり、セキュリティ境界はサーバーがその値をどう扱うかにあります。
PowerGrid の naturalSort() 機能は、リテラルのプレースホルダー {sortDirection} を含む生の ORDER BY 式を構築し、パイプラインがその文字列を orderByRaw() に渡す前に、そのプレースホルダーを 生の、検証されていないプロパティ値 に置き換えます。そのため、方向キーワードは SQL 内にそのまま埋め込まれます。
Laravel 自身の orderBy() は asc/desc 以外のものを拒否し、その検証によって通常のソート経路は安全になっています。問題は、同じ句に至る検証されていない第二の経路が存在することであり、攻撃者は検証経路を完全にスキップしてそこに到達できます(バイパス を参照)。
結果: ORDER BY 句内での任意の SQL。これはブラインドのブール型/時間ベースのオラクルとして悪用でき、データベースユーザーが読み取れる任意のデータを読み取ることが可能です。
naturalSort を使用する PowerGrid テーブルに到達できる者は誰でも、時間ベース/ブール型オラクルを使って、データベースから任意のデータ(他のテーブル、パスワードハッシュ、セッショントークン、API キー、テナント間のレコードなど)を読み取ることができます。
SELECT できるものはすべて完全に読み取れます。; UPDATE ... は実行されません。書き込みへの影響は、サブクエリが引き起こせる範囲に限定されます。SLEEP() や重いサブクエリを実行でき、データベーススレッドを簡単に占有できます。必要な権限は PR:L です。データテーブルは通常、アプリケーションの認証の背後にあるためです。影響を受けるテーブルが 認証されていない ページで描画される場合は、PR:N で再計算すると 8.2 High(高) になります。
3つのファイル、3つのステージ。すべての参照は脆弱なタグ 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()` は、`src/DataSource/Support/Sql.php` (60〜100行目) 内の
`getSortSqlByDriver()` によって構築されるドライバー別の式に解決されます。すべてのドライバーバリアントは
同じリテラルプレースホルダーで終わります:```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);
}
そして実行、52行目:```php
$query->{$method}($resolvedSql, $resolvedBindings); // $method === 'orderByRaw'
data_get($this->component, 'sortDirection') は攻撃者の文字列を返し、preg_replace_callback
はそれを SQL テキストに挿入し、orderByRaw() — 仕様上、その引数をエスケープしない —
はそれをデータベースに渡します。
すぐ下の行にある皮肉に注目してください: resolveBindings() (69行目) は存在し、naturalSort
は 'bindings' => [] を宣言しています。安全なパラメータ化の仕組みはまさにそこにあるのです。しかし、
方向キーワードには使えません — ORDER BY x ? は有効な SQL ではなく、方向をバインド
パラメータにすることは決してできないからです — まさにそのため、方向キーワードは代わりに許可リスト方式にしなければなりません。
POST /livewire/update ──▶ public string $sortDirection (Sorting.php:13, no validation) ──▶ data_get($component, 'sortDirection') (ColumnRawQueries.php:65) ──▶ "CAST(...) {sortDirection}" → "CAST(...) asc, (SELECT SLEEP(3))" ──▶ orderByRaw($sql) (ColumnRawQueries.php:52) ──▶ MySQL/MariaDB/PgSQL/SQLite/MSSQL
---
## 🔓 バイパス — Laravelのバリデーションでは救われない理由
ここが「生の補間」を実際に悪用可能なバグへと変える部分であり、成熟した広く使われているパッケージでこの問題が生き残った理由でもあります。
PowerGridはクエリを**パイプライン**で処理します。そのパイプラインの2つのステージがソート方向に触れます。
**`Sorting` パイプライン** — `src/DataSource/Processors/Database/Pipelines/Sorting.php`:```php
public function handle(mixed $query, Closure $next): mixed
{
// ...
if (filled($this->component->sortField)) { // line 21 <-- THE GUARD
if ($this->component->multiSort) {
$this->applyMultipleSort($query);
} else {
$this->applySingleSort($query, $this->component->sortField, $this->component->sortDirection);
}
}
return $next($query);
}
private function applySingleSort(..., string $sortField, string $direction): void
{
// ...
$query->orderBy($this->component->resolveSortField($sortField), $direction); // line 42
}
orderBy() は Laravel の 検証 API です。asc/desc 以外のものを渡すと、例外が
スローされます:```
InvalidArgumentException: Order direction must be "asc" or "desc".
つまり、通常の経路では — ユーザーが列ヘッダーをクリックし、`sortField=name`、`sortDirection=<payload>` —
フレームワークはインジェクションをブロックします。簡単な監査はここで止まり、「Laravel によって緩和されている」と結論づけます。
**`ColumnRawQueries` パイプライン** — 上に示した2番目のステージ — には**そのようなガードはありません**。見てください
その `handle()`(21〜27行目): 列を反復処理し、`rawQueries` を持つすべての列に対して、
それらを*無条件に*適用します。`sortField` を参照することはありません。`Sorting`
パイプラインの結果を参照することもありません。
この非対称性がバグです。
| `sortField` | `Sorting` パイプライン | `ColumnRawQueries` パイプライン | 結果 |
|---|---|---|---|
| `"name"` (入力あり) | 実行 → `orderBy()` が**検証** → ペイロードで例外をスロー | 実行 → インジェクション | ❌ 例外でブロック |
| `""` (空) | `filled('')` は `false` → **完全にスキップ** | 実行 → インジェクション | ✅ **インジェクションが成立** |