Description:
cursor.execute() corrupts or rejects valid SQL when a string literal contains the word “delimiter”
Severity / impact:
Data integrity. Worst case is silent corruption of a multi-row INSERT: the client rewrites the statement before sending it, so rows are truncated or dropped without the application seeing anything wrong beyond an eventual syntax error.
Version
Affected: 9.2.0, 9.7.0, 26.7.0 (all verified by running the reproducers below). Not affected: 9.0.0, 9.1.0 — these predate mysql/connector/_scripting.py.
The defect is in the pure-Python layer, so it reproduces with and without the C extension (use_pure=True and use_pure=False behave identically).
How to repeat:
Reproducer 1 — minimal, public API:
import mysql.connector
cnx = mysql.connector.connect(user=..., password=..., host=..., database=...)
cur = cnx.cursor()
cur.execute("SELECT %s", ("it's a separator don't",)) # OK
cur.execute("SELECT %s", ("it's a delimiter don't",)) # raises
The two calls differ by one word. The second raises:
InterfaceError: The backslash (\) character is not a valid delimiter.
The value is ordinary user data passed as a bound parameter. Nothing in it is a DELIMITER command.
Reproducer 2 — no server required, shows the corruption
from mysql.connector._scripting import MySQLScriptSplitter, split_multi_statement
stmt = (b"INSERT INTO t (id, txt) VALUES "
b"(1,'row one doesn\\'t matter'),"
b"(2,'csv sniffer does delimiter detection'),"
b"(3,'row three doesn\\'t matter')")
print(MySQLScriptSplitter.has_delimiter(stmt)) # True -- expected False
print(next(split_multi_statement(sql_code=stmt)))
Output:
True
{'single_stmts': deque([
b"INSERT INTO t (id, txt) VALUES (1,'row one doesn\\'t matter'),(2,'csv sniffer does",
b"three doesn\\'t matter')"]),
'mappable_stmt':
b"INSERT INTO t (id, txt) VALUES (1,'row one doesn\\'t matter'),(2,'csv sniffer does;\nthree doesn\\'t matter')"}
The splitter reads delimiter detection inside the string literal as a DELIMITER command, adopts the literal text detection as the new statement delimiter, and then splits the statement on every occurrence of it. Row 2’s value is truncated mid-literal and row 3’s opening is destroyed.
Root cause:
mysql/connector/_scripting.py:
DELIMITER_PATTERN: re.Pattern = re.compile(
rb"""(delimiter\s+)(?=(?:[^"'`]*(?:"[^"]*"|'[^']*'|`[^`]*`))*[^"'`]*$)""",
flags=re.IGNORECASE | re.MULTILINE,
)
cursor.execute() calls split_multi_statement() on the fully interpolated statement (cursor_cext.py:339, cursor.py:403), i.e. after parameters have been substituted, so caller data is parsed as if it were SQL.
Suggested fix:
• Do not run the script splitter on statements that were not submitted as scripts, or at minimum skip it when the statement contains no ; outside a literal.
• Recognise DELIMITER only as a command, i.e. at the start of a statement or line and outside any quoted literal — not by substring match anywhere.
• Replace the quote-parity lookahead with a real scan that tracks quoting state left to right and honours backslash escapes (and NO_BACKSLASH_ESCAPES), rather than a regex over the remaining text.
Description: cursor.execute() corrupts or rejects valid SQL when a string literal contains the word “delimiter” Severity / impact: Data integrity. Worst case is silent corruption of a multi-row INSERT: the client rewrites the statement before sending it, so rows are truncated or dropped without the application seeing anything wrong beyond an eventual syntax error. Version Affected: 9.2.0, 9.7.0, 26.7.0 (all verified by running the reproducers below). Not affected: 9.0.0, 9.1.0 — these predate mysql/connector/_scripting.py. The defect is in the pure-Python layer, so it reproduces with and without the C extension (use_pure=True and use_pure=False behave identically). How to repeat: Reproducer 1 — minimal, public API: import mysql.connector cnx = mysql.connector.connect(user=..., password=..., host=..., database=...) cur = cnx.cursor() cur.execute("SELECT %s", ("it's a separator don't",)) # OK cur.execute("SELECT %s", ("it's a delimiter don't",)) # raises The two calls differ by one word. The second raises: InterfaceError: The backslash (\) character is not a valid delimiter. The value is ordinary user data passed as a bound parameter. Nothing in it is a DELIMITER command. Reproducer 2 — no server required, shows the corruption from mysql.connector._scripting import MySQLScriptSplitter, split_multi_statement stmt = (b"INSERT INTO t (id, txt) VALUES " b"(1,'row one doesn\\'t matter')," b"(2,'csv sniffer does delimiter detection')," b"(3,'row three doesn\\'t matter')") print(MySQLScriptSplitter.has_delimiter(stmt)) # True -- expected False print(next(split_multi_statement(sql_code=stmt))) Output: True {'single_stmts': deque([ b"INSERT INTO t (id, txt) VALUES (1,'row one doesn\\'t matter'),(2,'csv sniffer does", b"three doesn\\'t matter')"]), 'mappable_stmt': b"INSERT INTO t (id, txt) VALUES (1,'row one doesn\\'t matter'),(2,'csv sniffer does;\nthree doesn\\'t matter')"} The splitter reads delimiter detection inside the string literal as a DELIMITER command, adopts the literal text detection as the new statement delimiter, and then splits the statement on every occurrence of it. Row 2’s value is truncated mid-literal and row 3’s opening is destroyed. Root cause: mysql/connector/_scripting.py: DELIMITER_PATTERN: re.Pattern = re.compile( rb"""(delimiter\s+)(?=(?:[^"'`]*(?:"[^"]*"|'[^']*'|`[^`]*`))*[^"'`]*$)""", flags=re.IGNORECASE | re.MULTILINE, ) cursor.execute() calls split_multi_statement() on the fully interpolated statement (cursor_cext.py:339, cursor.py:403), i.e. after parameters have been substituted, so caller data is parsed as if it were SQL. Suggested fix: • Do not run the script splitter on statements that were not submitted as scripts, or at minimum skip it when the statement contains no ; outside a literal. • Recognise DELIMITER only as a command, i.e. at the start of a statement or line and outside any quoted literal — not by substring match anywhere. • Replace the quote-parity lookahead with a real scan that tracks quoting state left to right and honours backslash escapes (and NO_BACKSLASH_ESCAPES), rather than a regex over the remaining text.