CVE-2026-32763
SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.
Kysely is a type-safe TypeScript SQL query builder. Versions up to and including 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. Version 0.28.12 fixes the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
8.2
SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.
Kysely is a type-safe TypeScript SQL query builder. Versions up to and including 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. Version 0.28.12 fixes the issue.
- Record
CVE-2026-32763- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-89
- Updated
- 2026-11-02
EUVDEUVD-2026-13392
8.2
Kysely is a type-safe TypeScript SQL query builder. Versions up to and including 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. Version 0.28.12 fixes the issue.
- Record
EUVD-2026-13392- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-wmrf-hv6w-mr66
8.2
SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.
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:
// 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)); // <-- NO single quote escaping
if (isArrayLocation) {
this.append(']');
}
}
Contrast with sanitizeIdentifier() in the same file, which properly doubles delimiter characters:
sanitizeIdentifier(identifier) {
const leftWrap = this.getLeftIdentifierWrapper();
const rightWrap = this.getRightIdentifierWrapper();
let sanitized = '';
for (const c of identifier) {
sanitized += c;
if (c === leftWrap) { sanitized += leftWrap; }
else if (c === rightWrap) { sanitized += rightWrap; }
}
return sanitized;
}
Both identifiers and JSON path keys are non-parameterizable SQL constructs that require manual escaping. Identifiers are protected; JSON path values are not.
PostgreSQL is not affected. The branching happens in JSONPathBuilder.#createBuilderWithPathLeg() (json-path-builder.js):
- MySQL/SQLite operators (
->$,->>$) produce aJSONPathNodetraversal →visitJSONPathLeg()concatenates the key directly into a single-quoted JSON path string ('$.key') — vulnerable, no escaping. - PostgreSQL operators (
->,->>) produce aJSONOperatorChainNodetraversal →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):
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…
- Record
GHSA-wmrf-hv6w-mr66- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-89
- Updated
- 2026-11-02
NVD
8.2
Kysely is a type-safe TypeScript SQL query builder. Versions up to and including 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. Version 0.28.12 fixes the issue.
- Record
CVE-2026-32763- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-89
- Updated
- 2026-11-02
OSV
8.2
SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.
Kysely is a type-safe TypeScript SQL query builder. Versions up to and including 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. Version 0.28.12 fixes the issue.
- Record
CVE-2026-32763- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-89
- Updated
- 2026-11-02
OSVGHSA-wmrf-hv6w-mr66
8.2
SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using `Kysely<any>`.
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:
// 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)); // <-- NO single quote escaping
if (isArrayLocation) {
this.append(']');
}
}
Contrast with sanitizeIdentifier() in the same file, which properly doubles delimiter characters:
sanitizeIdentifier(identifier) {
const leftWrap = this.getLeftIdentifierWrapper();
const rightWrap = this.getRightIdentifierWrapper();
let sanitized = '';
for (const c of identifier) {
sanitized += c;
if (c === leftWrap) { sanitized += leftWrap; }
else if (c === rightWrap) { sanitized += rightWrap; }
}
return sanitized;
}
Both identifiers and JSON path keys are non-parameterizable SQL constructs that require manual escaping. Identifiers are protected; JSON path values are not.
PostgreSQL is not affected. The branching happens in JSONPathBuilder.#createBuilderWithPathLeg() (json-path-builder.js):
- MySQL/SQLite operators (
->$,->>$) produce aJSONPathNodetraversal →visitJSONPathLeg()concatenates the key directly into a single-quoted JSON path string ('$.key') — vulnerable, no escaping. - PostgreSQL operators (
->,->>) produce aJSONOperatorChainNodetraversal →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):
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…
- Record
GHSA-wmrf-hv6w-mr66- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-89
- Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-32763 is a high vulnerability (CVSS 8.2) in Kysely, published March 18, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.44%, the 38th percentile. Fixed in kysely 0.28.12.
References
github.com/kysely-org/kysely/security/advisories/GHSA-wmrf-hv6w-mr66
github.com/kysely-org/kysely/commit/0a602bff2f442f6c26d5e047ca8f8715179f6d24
github.com/kysely-org/kysely/releases/tag/v0.28.12
nvd.nist.gov/vuln/detail/CVE-2026-32763
github.com/advisories/GHSA-wmrf-hv6w-mr66
github.com/CVEProject/cvelistV5/tree/main/cves/2026/32xxx/CVE-2026-32763.json
github.com/kysely-org/kysely
Left out here: the rest of 2 source descriptions. The JSON record has them.