Skip to content

Commit 1e79982

Browse files
committed
BUG#36476195: Incorrect escaping in pure Python mode if sql_mode includes NO_BACKSLASH_ESCAPES
This patch fixes the issue by adding proper conditional checks during statement conversions to check if NO_BACKSLASH_ESCAPES is included with/without other options in the sql_mode parameter and ensure correct string escaping is being done during conversion. Change-Id: I71372b881cd163be7714cf30c600fcafe976602c
1 parent 0a8019b commit 1e79982

File tree

5 files changed

+342
-19
lines changed

5 files changed

+342
-19
lines changed

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Copyright (c) 2009, 2024, Oracle and/or its affiliates.
88
Full release notes:
99
http://dev.mysql.com/doc/relnotes/connector-python/en/
1010

11+
v9.0.0
12+
======
13+
14+
- BUG#36476195: Incorrect escaping in pure Python mode if sql_mode includes NO_BACKSLASH_ESCAPES
15+
1116
v8.4.0
1217
======
1318

mysql-connector-python/lib/mysql/connector/aio/abstracts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def __init__(
190190
converter_str_fallback: bool = False,
191191
connection_timeout: int = DEFAULT_CONFIGURATION["connect_timeout"],
192192
unix_socket: Optional[str] = None,
193+
use_unicode: Optional[bool] = True,
193194
ssl_ca: Optional[str] = None,
194195
ssl_cert: Optional[str] = None,
195196
ssl_key: Optional[str] = None,
@@ -254,9 +255,9 @@ def __init__(
254255
self.raise_on_warnings: bool = raise_on_warnings
255256
self._buffered: bool = buffered
256257
self._raw: bool = raw
258+
self._use_unicode: bool = use_unicode
257259
self._have_next_result: bool = False
258260
self._unread_result: bool = False
259-
self._use_unicode: bool = True
260261
self._in_transaction: bool = False
261262
self._oci_config_file: Optional[str] = None
262263
self._oci_config_profile: Optional[str] = None

mysql-connector-python/lib/mysql/connector/aio/cursor.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,20 @@ async def close(self) -> bool:
172172
self._connection = None
173173
return True
174174

175-
def _process_params_dict(
175+
async def _process_params_dict(
176176
self, params: ParamsDictType
177177
) -> Dict[bytes, Union[bytes, Decimal]]:
178178
"""Process query parameters given as dictionary."""
179179
res: Dict[bytes, Any] = {}
180180
try:
181+
sql_mode = await self._connection.get_sql_mode()
181182
to_mysql = self._connection.converter.to_mysql
182183
escape = self._connection.converter.escape
183184
quote = self._connection.converter.quote
184185
for key, value in params.items():
185186
conv = value
186187
conv = to_mysql(conv)
187-
conv = escape(conv)
188+
conv = escape(conv, sql_mode)
188189
if not isinstance(value, Decimal):
189190
conv = quote(conv)
190191
res[key.encode()] = conv
@@ -194,17 +195,18 @@ def _process_params_dict(
194195
) from err
195196
return res
196197

197-
def _process_params(
198+
async def _process_params(
198199
self, params: ParamsSequenceType
199200
) -> Tuple[Union[bytes, Decimal], ...]:
200201
"""Process query parameters."""
201202
result = params[:]
202203
try:
204+
sql_mode = await self._connection.get_sql_mode()
203205
to_mysql = self._connection.converter.to_mysql
204206
escape = self._connection.converter.escape
205207
quote = self._connection.converter.quote
206208
result = [to_mysql(value) for value in result]
207-
result = [escape(value) for value in result]
209+
result = [escape(value, sql_mode) for value in result]
208210
result = [
209211
quote(value) if not isinstance(params[i], Decimal) else value
210212
for i, value in enumerate(result)
@@ -412,7 +414,7 @@ def _check_executed(self) -> None:
412414
if self._executed is None:
413415
raise InterfaceError(ERR_NO_RESULT_TO_FETCH)
414416

415-
def _prepare_statement(
417+
async def _prepare_statement(
416418
self,
417419
operation: StrOrBytes,
418420
params: Union[Sequence[Any], Dict[str, Any]] = (),
@@ -437,9 +439,11 @@ def _prepare_statement(
437439

438440
if params:
439441
if isinstance(params, dict):
440-
stmt = _bytestr_format_dict(stmt, self._process_params_dict(params))
442+
stmt = _bytestr_format_dict(
443+
stmt, await self._process_params_dict(params)
444+
)
441445
elif isinstance(params, (list, tuple)):
442-
psub = _ParamSubstitutor(self._process_params(params))
446+
psub = _ParamSubstitutor(await self._process_params(params))
443447
stmt = RE_PY_PARAM.sub(psub, stmt)
444448
if psub.remaining != 0:
445449
raise ProgrammingError(
@@ -461,7 +465,7 @@ async def _fetch_warnings(self) -> Optional[List[WarningType]]:
461465
result = await cur.fetchall()
462466
return result if result else None # type: ignore[return-value]
463467

464-
def _batch_insert(
468+
async def _batch_insert(
465469
self, operation: str, seq_params: Sequence[ParamsSequenceOrDictType]
466470
) -> Optional[bytes]:
467471
"""Implements multi row insert"""
@@ -496,9 +500,11 @@ def remove_comments(match: re.Match) -> str:
496500
for params in seq_params:
497501
tmp = fmt
498502
if isinstance(params, dict):
499-
tmp = _bytestr_format_dict(tmp, self._process_params_dict(params))
503+
tmp = _bytestr_format_dict(
504+
tmp, await self._process_params_dict(params)
505+
)
500506
else:
501-
psub = _ParamSubstitutor(self._process_params(params))
507+
psub = _ParamSubstitutor(await self._process_params(params))
502508
tmp = RE_PY_PARAM.sub(psub, tmp)
503509
if psub.remaining != 0:
504510
raise ProgrammingError(
@@ -686,7 +692,7 @@ async def execute(
686692
await self._connection.handle_unread_result()
687693
await self._reset_result()
688694

689-
stmt = self._prepare_statement(operation, params)
695+
stmt = await self._prepare_statement(operation, params)
690696
self._executed = stmt
691697

692698
try:
@@ -719,7 +725,7 @@ async def executemulti(
719725
await self._connection.handle_unread_result()
720726
await self._reset_result()
721727

722-
stmt = self._prepare_statement(operation, params)
728+
stmt = await self._prepare_statement(operation, params)
723729
self._executed = stmt
724730
self._executed_list = []
725731

@@ -752,7 +758,7 @@ async def executemany(
752758
if not seq_params:
753759
self._rowcount = 0
754760
return None
755-
stmt = self._batch_insert(operation, seq_params)
761+
stmt = await self._batch_insert(operation, seq_params)
756762
if stmt is not None:
757763
self._executed = stmt
758764
return await self.execute(stmt)

mysql-connector-python/lib/mysql/connector/conversion.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@
3838
from decimal import Decimal
3939
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
4040

41-
from .constants import MYSQL_VECTOR_TYPE_CODE, CharacterSet, FieldFlag, FieldType
41+
from .constants import (
42+
MYSQL_VECTOR_TYPE_CODE,
43+
CharacterSet,
44+
FieldFlag,
45+
FieldType,
46+
SQLMode,
47+
)
4248
from .custom_types import HexLiteral
4349
from .types import (
4450
DescriptionType,
@@ -141,7 +147,7 @@ def to_python(
141147
@staticmethod
142148
def escape(
143149
value: Any,
144-
sql_mode: Optional[str] = None, # pylint: disable=unused-argument
150+
sql_mode: Optional[Union[str, bytes]] = None, # pylint: disable=unused-argument
145151
) -> Any:
146152
"""Escape buffer for sending to MySQL"""
147153
return value
@@ -179,16 +185,19 @@ def __init__(
179185
] = {}
180186

181187
@staticmethod
182-
def escape(value: Any, sql_mode: Optional[str] = None) -> Any:
188+
def escape(value: Any, sql_mode: Optional[Union[str, bytes]] = None) -> Any:
183189
"""
184190
Escapes special characters as they are expected to by when MySQL
185191
receives them.
186192
As found in MySQL source mysys/charset.c
187193
188194
Returns the value if not a string, or the escaped string.
189195
"""
196+
if isinstance(sql_mode, bytes):
197+
# sql_mode is returned as bytes if use_unicode is set to False during connect()
198+
sql_mode = sql_mode.decode()
190199
if isinstance(value, (bytes, bytearray)):
191-
if sql_mode == "NO_BACKSLASH_ESCAPES":
200+
if sql_mode is not None and SQLMode.NO_BACKSLASH_ESCAPES in sql_mode:
192201
return value.replace(b"'", b"''")
193202
value = value.replace(b"\\", b"\\\\")
194203
value = value.replace(b"\n", b"\\n")
@@ -197,7 +206,7 @@ def escape(value: Any, sql_mode: Optional[str] = None) -> Any:
197206
value = value.replace(b"\042", b"\134\042") # double quotes
198207
value = value.replace(b"\032", b"\134\032") # for Win32
199208
elif isinstance(value, str) and not isinstance(value, HexLiteral):
200-
if sql_mode == "NO_BACKSLASH_ESCAPES":
209+
if sql_mode is not None and SQLMode.NO_BACKSLASH_ESCAPES in sql_mode:
201210
return value.replace("'", "''")
202211
value = value.replace("\\", "\\\\")
203212
value = value.replace("\n", "\\n")

0 commit comments

Comments
 (0)