diff --git a/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.test.ts b/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.test.ts index cd88886c2..9518a49ca 100644 --- a/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.test.ts +++ b/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.test.ts @@ -59,6 +59,62 @@ t.test("it understands MySQL escaping rules", async (t) => { [69, 71, "b"], ], ], + [ + `SELECT * FROM users WHERE name = 'John' -- and 'Jane';`, + [[33, 38, "John"]], + ], + [ + `SELECT * FROM users WHERE note = 'O\\'Reilly\\'s book';`, + [[33, 51, "O\\'Reilly\\'s book"]], + ], + [`SELECT * FROM users WHERE path = 'C:\\\\\\\\'Documents\\\\\\';`, []], + [ + `SELECT * FROM users WHERE name = 'Doe /* John */';`, + [[33, 48, "Doe /* John */"]], + ], + [ + `SELECT * FROM logs WHERE message = 'Error: Invalid path \\'/home/user/docs\\'\\\\nRetry with a valid path.';`, + [ + [ + 35, + 102, + "Error: Invalid path \\'/home/user/docs\\'\\\\nRetry with a valid path.", + ], + ], + ], + [ + `SELECT * FROM files WHERE data = x'4D7953514C' AND backup = b'01010101';`, + [ + [34, 45, "4D7953514C"], + [61, 70, "01010101"], + ], + ], + [ + `SELECT * FROM messages WHERE greeting = CONCAT('Hello, ', '\\'world!\\'', ' How\\'s everything?');`, + [ + [47, 55, "Hello, "], + [58, 69, "\\'world!\\'"], + [72, 92, " How\\'s everything?"], + ], + ], + [ + `SELECT * FROM products WHERE description = 'It''s ''quoted'' text here';`, + [[43, 70, "It''s ''quoted'' text here"]], + ], + [ + `SELECT * FROM code_snippets WHERE snippet = 'SELECT * FROM table -- where condition = \\'value\\'';`, + [[44, 95, "SELECT * FROM table -- where condition = \\'value\\'"]], + ], + [ + `SELECT * FROM reviews WHERE comment = 'He said, \\"This is awesome!\\" But I think it\\'s just \\'okay\\'.';`, + [ + [ + 38, + 101, + "He said, \\\"This is awesome!\\\" But I think it\\'s just \\'okay\\'.", + ], + ], + ], ]; for (const [input, expected] of checks) { diff --git a/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.ts b/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.ts index 0d4c3b675..bf6ff69bd 100644 --- a/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.ts +++ b/library/src/vulnerabilities/sql-injection/dialect/SQLDialectMySQL.ts @@ -13,68 +13,77 @@ export class SQLDialectMySQL implements SQLDialect { const char = sql[i]; const nextChar = sql[i + 1]; - // Handle escaping characters - if (char === "\\" && !inSingleLineComment && !inMultiLineComment) { + // Skip processing for escaped characters, ensuring we're not in a comment + if ( + char === "\\" && + !inSingleLineComment && + !inMultiLineComment && + literal + ) { i++; continue; } - // Check for single-line comment start + // Start or end of literal processing if ( - (char === "#" || (char === "-" && nextChar === "-")) && + escapeQuotes.includes(char) && + !inSingleLineComment && !inMultiLineComment ) { - inSingleLineComment = true; - continue; - } - - // Check for multi-line comment start - if (char === "/" && nextChar === "*" && !inSingleLineComment) { - inMultiLineComment = true; - i++; // Skip the '*' to avoid confusion with closing tags - continue; - } + if (literal && literal.quote === char) { + // Check for escape sequence of the quote itself + if (sql[i + 1] === char) { + i++; // Skip the next quote + continue; + } - // Handle end of single-line comment - if (inSingleLineComment && char === "\n") { - inSingleLineComment = false; - continue; - } + ranges.push([literal.start, i, sql.slice(literal.start + 1, i)]); + literal = undefined; // Exit literal + continue; + } - // Handle end of multi-line comment - if (inMultiLineComment && char === "*" && nextChar === "/") { - inMultiLineComment = false; - i++; // Move past the '/' - continue; + if (!literal) { + literal = { start: i, quote: char }; // Start a new literal + continue; + } } - // Skip literal processing if we're inside a comment - if (inSingleLineComment || inMultiLineComment) { - continue; - } + // Only check for comments if not inside a literal + if (!literal) { + // Single-line comment start + if ( + (char === "#" || (char === "-" && nextChar === "-")) && + !inMultiLineComment + ) { + inSingleLineComment = true; + continue; + } - // Literal processing - for (const quote of escapeQuotes) { - if (char === quote) { - if (literal && literal.quote === quote) { - // Double quote escape check - if (sql[i + 1] === quote) { - i++; - continue; - } + // Multi-line comment start + if (char === "/" && nextChar === "*" && !inSingleLineComment) { + inMultiLineComment = true; + i++; // Skip the '*' to avoid confusion with closing tags + continue; + } - ranges.push([literal.start, i, sql.slice(literal.start + 1, i)]); - literal = undefined; - continue; - } + // End of single-line comment + if (inSingleLineComment && char === "\n") { + inSingleLineComment = false; + continue; + } - literal = { start: i, quote: char }; + // End of multi-line comment + if (inMultiLineComment && char === "*" && nextChar === "/") { + inMultiLineComment = false; + i++; // Move past the '/' + continue; } } } - // If we end up with an unclosed literal, it's an error in the SQL syntax + // Check for unclosed literal as an error in SQL syntax if (literal) { + // Unclosed literal, return an empty range or handle as an error return []; }