Skip to content

Commit ed9e248

Browse files
committed
BUG#37447394: Unable to escape a parameter marker (%s) used in a query that should not
be treated as a parameter marker. This patch fixes this issue by adding proper regular expression checks to escape the `%s` parameter marker when `%%s` is used in the query. Change-Id: I31fce87821832cefa607105e8e0d20ca3be3f380
1 parent ec430bf commit ed9e248

File tree

5 files changed

+67
-1
lines changed

5 files changed

+67
-1
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ v9.3.0
1515
- WL#16327: Remove Cursors Prepared Raw and Named Tuple
1616
- BUG#37541353: (Contribution) Fix typing annotation of MySQLConnectionAbstract's close function
1717
- BUG#37453587: Github links in PyPI project's pages do not work
18+
- BUG#37447394: Unable to escape a parameter marker (`%s`) used in a query that should not be treated as a parameter marker
1819
- BUG#37418436: Arbitrary File Read in MySQL Python Client library
1920

2021
v9.2.0

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,8 @@ async def _prepare_statement(
410410
f"Could not process parameters: {type(params).__name__}({params}),"
411411
" it must be of type list, tuple or dict"
412412
)
413+
# final statement with `%%s` should be replaced as `%s`
414+
stmt = stmt.replace(b"%%s", b"%s")
413415

414416
return stmt
415417

@@ -1207,6 +1209,8 @@ async def execute(
12071209
except UnicodeEncodeError as err:
12081210
raise ProgrammingError(str(err)) from err
12091211

1212+
# final statement with `%%s` should be replaced as `%s`
1213+
operation = operation.replace(b"%%s", b"%s")
12101214
if b"%s" in operation:
12111215
# Convert %s to ? before sending it to MySQL
12121216
operation = re.sub(RE_SQL_FIND_PARAM, b"?", operation)

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
re.I | re.M | re.S,
9696
)
9797
RE_SQL_INSERT_VALUES = re.compile(r".*VALUES\s*(\(.+\)).*", re.I | re.M | re.S)
98-
RE_PY_PARAM = re.compile(b"(%s)")
98+
RE_PY_PARAM = re.compile(b"((?<!%)%s)")
9999
RE_PY_MAPPING_PARAM = re.compile(
100100
rb"""
101101
%
@@ -400,6 +400,8 @@ def execute(
400400
" it must be of type list, tuple or dict"
401401
)
402402

403+
# final statement with `%%s` should be replaced as `%s`
404+
stmt = stmt.replace(b"%%s", b"%s")
403405
self._stmt_partitions = split_multi_statement(
404406
sql_code=stmt, map_results=map_results
405407
)
@@ -1235,6 +1237,8 @@ def execute(
12351237
except UnicodeEncodeError as err:
12361238
raise ProgrammingError(str(err)) from err
12371239

1240+
# final statement with `%%s` should be replaced as `%s`
1241+
operation = operation.replace(b"%%s", b"%s")
12381242
if b"%s" in operation:
12391243
# Convert %s to ? before sending it to MySQL
12401244
operation = re.sub(RE_SQL_FIND_PARAM, b"?", operation)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ def execute(
334334
"Not all parameters were used in the SQL statement"
335335
)
336336

337+
# final statement with `%%s` should be replaced as `%s`
338+
stmt = stmt.replace(b"%%s", b"%s")
339+
337340
self._stmt_partitions = split_multi_statement(
338341
sql_code=stmt, map_results=map_results
339342
)
@@ -1121,6 +1124,8 @@ def execute(
11211124
except UnicodeDecodeError as err:
11221125
raise ProgrammingError(str(err)) from err
11231126

1127+
# final statement with `%%s` should be replaced as `%s`
1128+
operation = operation.replace("%%s", "%s")
11241129
if isinstance(params, dict):
11251130
replacement_keys = re.findall(RE_SQL_PYTHON_CAPTURE_PARAM_NAME, operation)
11261131
try:

mysql-connector-python/tests/test_bugs.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8899,3 +8899,55 @@ def test_connection_timeout(self):
88998899
cnx.cmd_query("SELECT SLEEP(5)")
89008900
except Exception as err:
89018901
self.fail(err)
8902+
8903+
8904+
class BugOra37447394(tests.MySQLConnectorTests):
8905+
"""BUG#37447394: Unable to escape a parameter marker (`%s`) used in a query that should not be
8906+
treated as a parameter marker.
8907+
8908+
The `%s` parameter marker is not being escaped by using `%%s` in an SQL statement being passed
8909+
via the cursor's execute() method. This is required while passing a query like:
8910+
`select date_format(%s, "%Y-%m-%d %H:%i:%%s")`. Currently, the `%s` at the end of the query is
8911+
is not being escaped and is being treated like a parameter marker instead.
8912+
8913+
This patch fixes this issue by adding proper regular expression checks to escape the `%s` by
8914+
using `%%s`.
8915+
"""
8916+
8917+
stmt = 'select date_format(%s, "%Y-%m-%d %H:%i:%%s")'
8918+
params = ("2017-06-15 12:20:23",)
8919+
8920+
@foreach_cnx()
8921+
def test_bug37447394(self):
8922+
try:
8923+
with self.cnx.cursor() as cur:
8924+
cur.execute(self.stmt, self.params)
8925+
self.assertEqual(cur.fetchone(), self.params)
8926+
with self.cnx.cursor(prepared=True) as cur:
8927+
cur.execute(self.stmt, self.params)
8928+
self.assertEqual(cur.fetchone(), self.params)
8929+
except Exception as err:
8930+
self.fail(err)
8931+
8932+
8933+
class BugOra37447394_async(tests.MySQLConnectorAioTestCase):
8934+
"""BUG#37447394: Unable to escape a parameter marker (`%s`) used in a query that should not be
8935+
treated as a parameter marker.
8936+
8937+
For more details check `test_bugs.BugOra37447394`.
8938+
"""
8939+
8940+
stmt = 'select date_format(%s, "%Y-%m-%d %H:%i:%%s")'
8941+
params = ("2017-06-15 12:20:23", )
8942+
8943+
@foreach_cnx_aio()
8944+
async def test_bug37447394_async(self):
8945+
try:
8946+
async with await self.cnx.cursor() as cur:
8947+
await cur.execute(self.stmt, self.params)
8948+
self.assertEqual(await cur.fetchone(), self.params)
8949+
async with await self.cnx.cursor(prepared=True) as cur:
8950+
await cur.execute(self.stmt, self.params)
8951+
self.assertEqual(await cur.fetchone(), self.params)
8952+
except Exception as err:
8953+
self.fail(err)

0 commit comments

Comments
 (0)