From cc633a4c7d311ab10e20241ca7c9488acd26f349 Mon Sep 17 00:00:00 2001 From: Austin Bergstrom Date: Tue, 14 Jul 2026 13:55:04 -0400 Subject: [PATCH 1/2] fix(lexer): don't treat backslash as string escape in SQL Server and Oracle SQL Server (T-SQL) and Oracle follow ANSI-SQL string semantics: backslash is an ordinary character and a quote inside a string literal is escaped by doubling it (''). The lexer, however, unconditionally treated '\' as an escape character in scanStringWithDelimiter. As a result a literal like ESCAPE '\' (contents: a single backslash) had its closing quote treated as escaped, so scanning continued and swallowed the rest of the batch up to the next quote. With two such literals the text between them collapsed into one string, truncating the obfuscated query customers saw in DBM (SDBM-2792). A previous special case only rescued this when the query ended with '\'; the bug remained whenever more SQL followed. Disable backslash-escaping for the two ANSI-flavored dialects. MySQL and Snowflake genuinely use C-style backslash escaping and are left unchanged. PostgreSQL ordinary strings also do not backslash-escape, but its E'...' escape-string syntax does and the lexer cannot yet distinguish the two, so Postgres is deferred to a follow-up. Tests (all synthetic queries, no customer data): - Tokenizer specs pinning '\' as its own STRING token mid-query for SQL Server and Oracle. - Obfuscator regressions using the two-literal shape (SQL to both between and after the escapes) for SQL Server and Oracle. - Boundary guards asserting MySQL and Snowflake still escape, proving the fix did not over-broaden. Co-Authored-By: Claude Opus 4.8 (1M context) --- obfuscator_test.go | 34 +++++++++++++++++++++ sqllexer.go | 6 +++- sqllexer_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/obfuscator_test.go b/obfuscator_test.go index df3dbf0..a531588 100644 --- a/obfuscator_test.go +++ b/obfuscator_test.go @@ -45,6 +45,40 @@ func TestObfuscator(t *testing.T) { expected: "SELECT * FROM users where id = ?", replaceDigits: false, }, + { + // SDBM-2792: SQL Server does not treat backslash as a string escape, so + // ESCAPE N'\' is a complete literal (a single backslash). This exercises + // the problematic shape: two ESCAPE N'\' literals with SQL both between + // and after them. Without the fix the first '\' escaped its closing quote + // and consumed forward to the opening quote of the second literal, so the + // text in between collapsed into one string and the query was truncated. + input: `DECLARE @p1 NVARCHAR(50)=N'%foo%', @p2 NVARCHAR(50)=N'%bar%'; SELECT col FROM tbl WHERE col LIKE @p1 ESCAPE N'\' AND col LIKE @p2 ESCAPE N'\';`, + expected: `DECLARE @p1 NVARCHAR(?)=N?, @p2 NVARCHAR(?)=N?; SELECT col FROM tbl WHERE col LIKE @p1 ESCAPE N? AND col LIKE @p2 ESCAPE N?;`, + dbms: DBMSSQLServer, + }, + { + // SDBM-2792: Oracle does not treat backslash as a string escape either. + // Same two-literal shape as the SQL Server case above. + input: `SELECT col FROM tbl WHERE col LIKE '%foo\_%' ESCAPE '\' AND col LIKE '%bar\_%' ESCAPE '\' AND flag = 'x'`, + expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ? AND col LIKE ? ESCAPE ? AND flag = ?`, + dbms: DBMSOracle, + }, + { + // Boundary guard: MySQL genuinely uses backslash as a string escape, so + // '\' escapes the following quote and the literal continues (you would + // write '\\' for a literal backslash). Asserting the trailing text is + // absorbed proves the SQL Server / Oracle fix did not over-broaden. + input: `SELECT col FROM tbl WHERE col LIKE '%foo%' ESCAPE '\' AND flag = 1`, + expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ?`, + dbms: DBMSMySQL, + }, + { + // Boundary guard: Snowflake also supports backslash escape sequences, so + // it must keep the escaping behavior too. + input: `SELECT col FROM tbl WHERE col LIKE '%foo%' ESCAPE '\' AND flag = 1`, + expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ?`, + dbms: DBMSSnowflake, + }, { input: "SELECT * FROM \"users table\" where id = 1", expected: "SELECT * FROM \"users table\" where id = ?", diff --git a/sqllexer.go b/sqllexer.go index 9ee985b..f4bd298 100644 --- a/sqllexer.go +++ b/sqllexer.go @@ -318,6 +318,10 @@ func (s *Lexer) scanStringWithDelimiter(delimiter rune) *Token { escaped := false escapedQuote := false + // SQL Server (T-SQL) and Oracle do not use backslash as a string escape + // character; a quote inside a literal is escaped by doubling it (''). + backslashEscapes := s.config.DBMS != DBMSSQLServer && s.config.DBMS != DBMSOracle + ch := s.next() // consume opening quote for ; !isEOF(ch); ch = s.next() { @@ -327,7 +331,7 @@ func (s *Lexer) scanStringWithDelimiter(delimiter rune) *Token { continue } - if ch == '\\' { + if backslashEscapes && ch == '\\' { escaped = true continue } diff --git a/sqllexer_test.go b/sqllexer_test.go index 1a127f4..b950a01 100644 --- a/sqllexer_test.go +++ b/sqllexer_test.go @@ -1027,6 +1027,79 @@ here */`, {STRING, `'\'`}, }, }, + { + // In SQL Server, backslash is not a string escape character + // (quotes are escaped by doubling: ''). A string literal containing a + // lone backslash, e.g. ESCAPE '\', must terminate at its closing quote + // even when more SQL follows. + name: "sqlserver escape backslash mid-query", + input: `SELECT * FROM t WHERE x LIKE '%a%' ESCAPE '\' AND y = 1`, + expected: []TokenSpec{ + {COMMAND, "SELECT"}, + {SPACE, " "}, + {WILDCARD, "*"}, + {SPACE, " "}, + {KEYWORD, "FROM"}, + {SPACE, " "}, + {IDENT, "t"}, + {SPACE, " "}, + {KEYWORD, "WHERE"}, + {SPACE, " "}, + {IDENT, "x"}, + {SPACE, " "}, + {KEYWORD, "LIKE"}, + {SPACE, " "}, + {STRING, `'%a%'`}, + {SPACE, " "}, + {KEYWORD, "ESCAPE"}, + {SPACE, " "}, + {STRING, `'\'`}, + {SPACE, " "}, + {KEYWORD, "AND"}, + {SPACE, " "}, + {IDENT, "y"}, + {SPACE, " "}, + {OPERATOR, "="}, + {SPACE, " "}, + {NUMBER, "1"}, + }, + lexerOpts: []lexerOption{WithDBMS(DBMSSQLServer)}, + }, + { + // Oracle also does not treat backslash as a string escape character. + name: "oracle escape backslash mid-query", + input: `SELECT * FROM t WHERE x LIKE '%a%' ESCAPE '\' AND y = 1`, + expected: []TokenSpec{ + {COMMAND, "SELECT"}, + {SPACE, " "}, + {WILDCARD, "*"}, + {SPACE, " "}, + {KEYWORD, "FROM"}, + {SPACE, " "}, + {IDENT, "t"}, + {SPACE, " "}, + {KEYWORD, "WHERE"}, + {SPACE, " "}, + {IDENT, "x"}, + {SPACE, " "}, + {KEYWORD, "LIKE"}, + {SPACE, " "}, + {STRING, `'%a%'`}, + {SPACE, " "}, + {KEYWORD, "ESCAPE"}, + {SPACE, " "}, + {STRING, `'\'`}, + {SPACE, " "}, + {KEYWORD, "AND"}, + {SPACE, " "}, + {IDENT, "y"}, + {SPACE, " "}, + {OPERATOR, "="}, + {SPACE, " "}, + {NUMBER, "1"}, + }, + lexerOpts: []lexerOption{WithDBMS(DBMSOracle)}, + }, { name: "simple select with multiline comments as separators", input: `SELECT/**/*/**/FROM/**/test`, From 82a7a2a40e3185fc18a9b1c3aed97453391cc397 Mon Sep 17 00:00:00 2001 From: Austin Bergstrom Date: Tue, 14 Jul 2026 14:06:25 -0400 Subject: [PATCH 2/2] fix comment --- obfuscator_test.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/obfuscator_test.go b/obfuscator_test.go index a531588..7dd2d2a 100644 --- a/obfuscator_test.go +++ b/obfuscator_test.go @@ -46,35 +46,26 @@ func TestObfuscator(t *testing.T) { replaceDigits: false, }, { - // SDBM-2792: SQL Server does not treat backslash as a string escape, so - // ESCAPE N'\' is a complete literal (a single backslash). This exercises - // the problematic shape: two ESCAPE N'\' literals with SQL both between - // and after them. Without the fix the first '\' escaped its closing quote - // and consumed forward to the opening quote of the second literal, so the - // text in between collapsed into one string and the query was truncated. + // SQL Server does not treat backslash as a string escape, so + // ESCAPE N'\' is a complete literal (a single backslash). input: `DECLARE @p1 NVARCHAR(50)=N'%foo%', @p2 NVARCHAR(50)=N'%bar%'; SELECT col FROM tbl WHERE col LIKE @p1 ESCAPE N'\' AND col LIKE @p2 ESCAPE N'\';`, expected: `DECLARE @p1 NVARCHAR(?)=N?, @p2 NVARCHAR(?)=N?; SELECT col FROM tbl WHERE col LIKE @p1 ESCAPE N? AND col LIKE @p2 ESCAPE N?;`, dbms: DBMSSQLServer, }, { - // SDBM-2792: Oracle does not treat backslash as a string escape either. - // Same two-literal shape as the SQL Server case above. + // Oracle does not treat backslash as a string escape either. input: `SELECT col FROM tbl WHERE col LIKE '%foo\_%' ESCAPE '\' AND col LIKE '%bar\_%' ESCAPE '\' AND flag = 'x'`, expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ? AND col LIKE ? ESCAPE ? AND flag = ?`, dbms: DBMSOracle, }, { - // Boundary guard: MySQL genuinely uses backslash as a string escape, so - // '\' escapes the following quote and the literal continues (you would - // write '\\' for a literal backslash). Asserting the trailing text is - // absorbed proves the SQL Server / Oracle fix did not over-broaden. + // MySQL genuinely uses backslash as a string escape input: `SELECT col FROM tbl WHERE col LIKE '%foo%' ESCAPE '\' AND flag = 1`, expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ?`, dbms: DBMSMySQL, }, { - // Boundary guard: Snowflake also supports backslash escape sequences, so - // it must keep the escaping behavior too. + // Snowflake also supports backslash escape sequences input: `SELECT col FROM tbl WHERE col LIKE '%foo%' ESCAPE '\' AND flag = 1`, expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ?`, dbms: DBMSSnowflake,