Skip to content

MySQL: stringQuote doesn't escape backslashes — FixedLiteral values can break out of string literals (injection) #609

Description

@krishna3554

Summary

SQLBuilder.stringQuote() — the fallback serializer for constant SQL arguments — escapes single quotes by doubling (''') but leaves backslashes untouched. Under MySQL's default sql_mode (where NO_BACKSLASH_ESCAPES is off), a backslash inside a string literal is an escape character. A user-supplied value containing \ followed by ' therefore breaks out of the literal, making FixedLiteral (and Debug-mode serialization) an SQL-injection vector on the MySQL dialect.

Location

  • File: internal/jet/sql_builder.go
  • Function: stringQuote (line ~324), used by argToString (line ~250) via insertConstantArgument
  • MySQL dialect hook mysql/dialect.go argumentToString only special-cases []byte; strings fall through to the generic quoting
func stringQuote(value string) string {
    return `'` + strings.Replace(value, "'", "''", -1) + `'`
}

Reachable in production through:

  • jet.FixedLiteral(value) — documented as "injected directly to SQL query"literalSerializer{constant: true}insertConstantArgumentargToString
  • Any statement serialized with Debug: true, where even parametrized arguments take the constant path

(Note: a comment near the driver.Valuer branch says argToString "is called only from DebugSQL" — that no longer holds for FixedLiteral.)

Problem

PostgreSQL and SQLite treat backslashes literally inside '...' (with standard-conforming strings), so quote-doubling is sufficient there. MySQL does not: with default settings, \ escapes the next character.

Injection trace for user input:

\'; DROP TABLE sensitive; --

stringQuote produces:

'\'; DROP TABLE sensitive; --'

MySQL parses this as the string \ (backslash escapes the closing quote), statement terminator, then DROP TABLE sensitive; — arbitrary SQL executes. The '' doubling never engages because the quote is neutralized by the preceding backslash first.

The same defect also silently corrupts legitimate values containing trailing backslashes (e.g. Windows paths C:\dir\'C:\dir\' → unterminated/merged literals).

Trigger / Reproduction

Static analysis finding — behavior derived from source at master (012e92c6); not confirmed by execution:

stmt := mysql.SELECT(jet.Raw("1")).
    WHERE(mysql.StringExp(jet.FixedLiteral(`\'; DROP TABLE x; --`)))
// stmt.DebugSQL() (or FixedLiteral serialization against MySQL) yields:
// SELECT 1 WHERE '\'; DROP TABLE x; --';

Executed against MySQL ≥5.x with default sql_mode, the DROP TABLE runs.

Expected Behavior

Dialect-aware string escaping. For MySQL either:

  • escape backslashes as well: strings.NewReplacer("\\", "\\\\", "'", "''"), or
  • document/enforce that constant serialization requires NO_BACKSLASH_ESCAPES, or
  • encode as hex literal like the existing []byte path (X'...').

PostgreSQL/SQLite behavior can stay as-is.

Actual Behavior

Backslash-terminated quotes are treated as string delimiters, enabling injection through any value passed to FixedLiteral (or rendered via DebugSQL) on the MySQL dialect.

Impact

SQL injection / query corruption for MySQL users who use FixedLiteral with any externally influenced string — precisely the API's advertised purpose ("does not appear in parametrized argument list"). Severity is high for the MySQL dialect; PostgreSQL and SQLite are unaffected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions