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} → insertConstantArgument → argToString
- 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.
Summary
SQLBuilder.stringQuote()— the fallback serializer for constant SQL arguments — escapes single quotes by doubling ('→'') but leaves backslashes untouched. Under MySQL's defaultsql_mode(whereNO_BACKSLASH_ESCAPESis off), a backslash inside a string literal is an escape character. A user-supplied value containing\followed by'therefore breaks out of the literal, makingFixedLiteral(and Debug-mode serialization) an SQL-injection vector on the MySQL dialect.Location
internal/jet/sql_builder.gostringQuote(line ~324), used byargToString(line ~250) viainsertConstantArgumentmysql/dialect.goargumentToStringonly special-cases[]byte; strings fall through to the generic quotingReachable in production through:
jet.FixedLiteral(value)— documented as "injected directly to SQL query" →literalSerializer{constant: true}→insertConstantArgument→argToStringDebug: true, where even parametrized arguments take the constant path(Note: a comment near the
driver.Valuerbranch saysargToString"is called only from DebugSQL" — that no longer holds forFixedLiteral.)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:
stringQuoteproduces:'\'; DROP TABLE sensitive; --'MySQL parses this as the string
\(backslash escapes the closing quote), statement terminator, thenDROP 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:Executed against MySQL ≥5.x with default
sql_mode, theDROP TABLEruns.Expected Behavior
Dialect-aware string escaping. For MySQL either:
strings.NewReplacer("\\", "\\\\", "'", "''"), orNO_BACKSLASH_ESCAPES, or[]bytepath (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 viaDebugSQL) on the MySQL dialect.Impact
SQL injection / query corruption for MySQL users who use
FixedLiteralwith 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.