GitHub Security Advisory: GHSA-8cpq-38p9-67gx
Package: kysely (NPM)
Severity: HIGH (CVSS 8.1)
CWE: CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Vulnerable versions: <= 0.28.13
Patched in: 0.28.14

Summary: Kysely has a MySQL SQL Injection via Insufficient Backslash Escaping in `sql.lit(string)` usage or similar methods that append string literal values into the compiled SQL strings

Details:
## Summary

Kysely's `DefaultQueryCompiler.sanitizeStringLiteral()` only escapes single quotes by doubling them (`'` → `''`) but does not escape backslashes. When used with the MySQL dialect (where `NO_BACKSLASH_ESCAPES` is OFF by default), an attacker can use a backslash to escape the trailing quote of a string literal, breaking out of the string context and injecting arbitrary SQL. This affects any code path that uses `ImmediateValueTransformer` to inline values — specifically `CreateIndexBuilder.where()` and `CreateViewBuilder.as()`.

## Details

The root cause is in `DefaultQueryCompiler.sanitizeStringLiteral()`:

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

Where `LIT_WRAP_REGEX` is defined as `/'/g` (line 121). This only doubles single quotes — it does not escape backslash characters.

The function is called from `appendStringLiteral()` which wraps the sanitized value in single quotes:

**`src/query-compiler/default-query-compiler.ts:1841-1845`**
```typescript
protected appendStringLiteral(value: string): void {
  this.append("'")
  this.append(this.sanitizeStringLiteral(value))
  this.append("'")
}
```

This is reached when `visitValue()` encounters an immediate value node (line 525-527), which is created by `ImmediateValueTransformer` used in `CreateIndexBuilder.where()`:

**`src/schema/create-index-builder.ts:266-278`**
```typescript
where(...args: any[]): any {
  const transformer = new ImmediateValueTransformer()

  return new CreateIndexBuilder({
    ...this.#props,
    node: QueryNode.cloneWithWhere(
      this.#props.node,
      transformer.transformNode(
        parseValueBinaryOperationOrExpression(args),
        this.#props.queryId,
      ),
    ),
  })
}
```

The `MysqlQueryCompiler` (at `src/dialect/mysql/mysql-query-compiler.ts:6-75`) extends `DefaultQueryCompiler` but does **not** override `sanitizeStringLiteral`, inheriting the backslash-unaware implementation.

**Exploitation mechanism:**

In MySQL with the default `NO_BACKSLASH_ESCAPES=OFF` setting, the backslash character (`\`) acts as an escape character inside string literals. Given input `\' OR 1=1 --`:

1. `sanitizeStringLiteral` doubles the quote: `\'' OR 1=1 --`
2. `appendStringLiteral` wraps: `'\'' OR 1=1 --'`
3. MySQL interprets `\'` as an escaped (literal) single quote, so the string content is `'` and the second `'` closes the string
4. ` OR 1=1 --` is parsed as SQL

## PoC

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

interface Database {
  orders: {
    id: number
    status: string
    order_nr: string
  }
}

const db = new Kysely<Database>({
  dialect: new MysqlDialect({
    pool: createPool({
      host: 'localhost',
      database: 'test',
      user: 'root',
      [CREDENTIAL_REDACTED]',
    }),
  }),
})

// Simulates user-controlled input reaching CreateIndexBuilder.wh