Skip to content

Commit def354c

Browse files
committed
BUG22529828 Fix potential SQL injection
This patch fixes a potential SQL injection that may occur when the parameters expansion is done in multiple steps and under some circumstances a substring in an incoming parameter value can be expanded several times.
1 parent fd825b1 commit def354c

File tree

3 files changed

+62
-26
lines changed

3 files changed

+62
-26
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ v2.1.4
1313

1414
- BUG#22873551: Fix cleartext authentication issue
1515
- BUG#22545879: Fix usage of --ssl-cipher option
16+
- BUG#22529828: Fix potencial SQL injection
1617
- BUG#21881038: Fix duplicate entry in CHANGES.txt
1718
- BUG#21879914: Fix using SSL without key or certificate using C/Ext
1819
- BUG#21879859: Fix consuming results after calling procedure

lib/mysql/connector/cursor.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# MySQL Connector/Python - MySQL driver written in Python.
2-
# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
33

44
# MySQL Connector/Python is licensed under the terms of the GPLv2
55
# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
@@ -44,6 +44,14 @@
4444
re.I | re.M | re.S)
4545
RE_SQL_INSERT_VALUES = re.compile(r'.*VALUES\s*(\(.*\)).*', re.I | re.M | re.S)
4646
RE_PY_PARAM = re.compile(b'(%s)')
47+
RE_PY_MAPPING_PARAM = re.compile(
48+
br'''
49+
%
50+
\((?P<mapping_key>[^)]+)\)
51+
(?P<conversion_type>[diouxXeEfFgGcrs%])
52+
''',
53+
re.X
54+
)
4755
RE_SQL_SPLIT_STMTS = re.compile(
4856
b''';(?=(?:[^"'`]*["'`][^"'`]*["'`])*[^"'`]*$)''')
4957
RE_SQL_FIND_PARAM = re.compile(
@@ -75,6 +83,29 @@ def remaining(self):
7583
return len(self.params) - self.index
7684

7785

86+
def _bytestr_format_dict(bytestr, value_dict):
87+
"""
88+
>>> _bytestr_format_dict(b'%(a)s', {b'a': b'foobar'})
89+
b'foobar
90+
>>> _bytestr_format_dict(b'%%(a)s', {b'a': b'foobar'})
91+
b'%%(a)s'
92+
>>> _bytestr_format_dict(b'%%%(a)s', {b'a': b'foobar'})
93+
b'%%foobar'
94+
>>> _bytestr_format_dict(b'%(x)s %(y)s',
95+
... {b'x': b'x=%(y)s', b'y': b'y=%(x)s'})
96+
b'x=%(y)s y=%(x)s'
97+
"""
98+
def replace(matchobj):
99+
groups = matchobj.groupdict()
100+
if groups["conversion_type"] == b"%":
101+
return b"%"
102+
if groups["conversion_type"] == b"s":
103+
return value_dict[groups["mapping_key"]]
104+
raise ValueError("Unsupported conversion_type: {0}"
105+
"".format(groups["conversion_type"]))
106+
return RE_PY_MAPPING_PARAM.sub(replace, bytestr)
107+
108+
78109
class CursorBase(MySQLCursorAbstract):
79110
"""
80111
Base for defining MySQLCursor. This class is a skeleton and defines
@@ -360,9 +391,9 @@ def _process_params_dict(self, params):
360391
conv = escape(conv)
361392
conv = quote(conv)
362393
if PY2:
363-
res["%({0})s".format(key)] = conv
394+
res[key] = conv
364395
else:
365-
res["%({0})s".format(key).encode()] = conv
396+
res[key.encode()] = conv
366397
except Exception as err:
367398
raise errors.ProgrammingError(
368399
"Failed processing pyformat-parameters; %s" % err)
@@ -497,8 +528,8 @@ def execute(self, operation, params=None, multi=False):
497528

498529
if params is not None:
499530
if isinstance(params, dict):
500-
for key, value in self._process_params_dict(params).items():
501-
stmt = stmt.replace(key, value)
531+
stmt = _bytestr_format_dict(
532+
stmt, self._process_params_dict(params))
502533
elif isinstance(params, (list, tuple)):
503534
psub = _ParamSubstitutor(self._process_params(params))
504535
stmt = RE_PY_PARAM.sub(psub, stmt)
@@ -551,8 +582,8 @@ def remove_comments(match):
551582
for params in seq_params:
552583
tmp = fmt
553584
if isinstance(params, dict):
554-
for key, value in self._process_params_dict(params).items():
555-
tmp = tmp.replace(key, value)
585+
tmp = _bytestr_format_dict(
586+
tmp, self._process_params_dict(params))
556587
else:
557588
psub = _ParamSubstitutor(self._process_params(params))
558589
tmp = RE_PY_PARAM.sub(psub, tmp)

tests/test_cursor.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
33

44
# MySQL Connector/Python is licensed under the terms of the GPLv2
55
# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
@@ -421,28 +421,32 @@ def test__process_params_dict(self):
421421
'p': datetime.time(20, 3, 23),
422422
'q': st_now,
423423
'r': datetime.timedelta(hours=40, minutes=30, seconds=12),
424+
's': 'foo %(t)s',
425+
't': 'foo %(s)s',
424426
}
425427
exp = {
426-
b'%(a)s': b'NULL',
427-
b'%(b)s': b'128',
428-
b'%(c)s': b'1281288',
429-
b'%(d)s': repr(float(3.14)) if PY2 else b'3.14',
430-
b'%(e)s': b"'3.14'",
431-
b'%(f)s': b"'back\\\\slash'",
432-
b'%(g)s': b"'newline\\n'",
433-
b'%(h)s': b"'return\\r'",
434-
b'%(i)s': b"'\\'single\\''",
435-
b'%(j)s': b'\'\\"double\\"\'',
436-
b'%(k)s': b"'windows\\\x1a'",
437-
b'%(l)s': b"'Strings are sexy'",
438-
b'%(m)s': b"'\xe8\x8a\xb1'",
439-
b'%(n)s': b"'2008-05-07 20:01:23'",
440-
b'%(o)s': b"'2008-05-07'",
441-
b'%(p)s': b"'20:03:23'",
442-
b'%(q)s': b"'" +
428+
b'a': b'NULL',
429+
b'b': b'128',
430+
b'c': b'1281288',
431+
b'd': repr(float(3.14)) if PY2 else b'3.14',
432+
b'e': b"'3.14'",
433+
b'f': b"'back\\\\slash'",
434+
b'g': b"'newline\\n'",
435+
b'h': b"'return\\r'",
436+
b'i': b"'\\'single\\''",
437+
b'j': b'\'\\"double\\"\'',
438+
b'k': b"'windows\\\x1a'",
439+
b'l': b"'Strings are sexy'",
440+
b'm': b"'\xe8\x8a\xb1'",
441+
b'n': b"'2008-05-07 20:01:23'",
442+
b'o': b"'2008-05-07'",
443+
b'p': b"'20:03:23'",
444+
b'q': b"'" +
443445
time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii')
444446
+ b"'",
445-
b'%(r)s': b"'40:30:12'",
447+
b'r': b"'40:30:12'",
448+
b's': b"'foo %(t)s'",
449+
b't': b"'foo %(s)s'",
446450
}
447451

448452
self.cnx = connection.MySQLConnection(**tests.get_mysql_config())

0 commit comments

Comments
 (0)