Skip to content

Commit c026c37

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 7be8e73 commit c026c37

File tree

3 files changed

+80
-26
lines changed

3 files changed

+80
-26
lines changed

CHANGES.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ MySQL Connector/Python 2.0 - Release Notes & Changes
33
====================================================
44

55
MySQL Connector/Python
6-
Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved.
6+
Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
77

88
Full release notes:
99
http://dev.mysql.com/doc/relnotes/connector-python/en/
1010

11+
12+
v2.0.5
13+
======
14+
- BUG22529828: Fix potencial SQL injection
15+
1116
v2.0.4
1217
======
1318

lib/mysql/connector/cursor.py

Lines changed: 47 additions & 6 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, 2014, 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
@@ -29,6 +29,7 @@
2929
import weakref
3030

3131
from . import errors
32+
from .catch23 import PY2
3233

3334
SQL_COMMENT = r"\/\*.*?\*\/"
3435
RE_SQL_COMMENT = re.compile(
@@ -42,6 +43,14 @@
4243
re.I | re.M | re.S)
4344
RE_SQL_INSERT_VALUES = re.compile(r'.*VALUES\s*(\(.*\)).*', re.I | re.M | re.S)
4445
RE_PY_PARAM = re.compile(b'(%s)')
46+
RE_PY_MAPPING_PARAM = re.compile(
47+
br'''
48+
%
49+
\((?P<mapping_key>[^)]+)\)
50+
(?P<conversion_type>[diouxXeEfFgGcrs%])
51+
''',
52+
re.X
53+
)
4554
RE_SQL_SPLIT_STMTS = re.compile(
4655
b''';(?=(?:[^"'`]*["'`][^"'`]*["'`])*[^"'`]*$)''')
4756
RE_SQL_FIND_PARAM = re.compile(
@@ -73,6 +82,35 @@ def remaining(self):
7382
return len(self.params) - self.index
7483

7584

85+
def _bytestr_format_dict(bytestr, value_dict):
86+
"""
87+
>>> _bytestr_format_dict(b'%(a)s', {b'a': b'foobar'})
88+
b'foobar
89+
>>> _bytestr_format_dict(b'%%(a)s', {b'a': b'foobar'})
90+
b'%%(a)s'
91+
>>> _bytestr_format_dict(b'%%%(a)s', {b'a': b'foobar'})
92+
b'%%foobar'
93+
>>> _bytestr_format_dict(b'%(x)s %(y)s',
94+
... {b'x': b'x=%(y)s', b'y': b'y=%(x)s'})
95+
b'x=%(y)s y=%(x)s'
96+
"""
97+
def replace(matchobj):
98+
value = None
99+
groups = matchobj.groupdict()
100+
if groups["conversion_type"] == b"%":
101+
value = b"%"
102+
if groups["conversion_type"] == b"s":
103+
key = groups["mapping_key"].encode("utf-8") \
104+
if PY2 else groups["mapping_key"]
105+
value = value_dict[key]
106+
if value is None:
107+
raise ValueError("Unsupported conversion_type: {0}"
108+
"".format(groups["conversion_type"]))
109+
return value.decode("utf-8") if PY2 else value
110+
return RE_PY_MAPPING_PARAM.sub(replace, bytestr.decode("utf-8")
111+
if PY2 else bytestr)
112+
113+
76114
class CursorBase(object):
77115
"""
78116
Base for defining MySQLCursor. This class is a skeleton and defines
@@ -355,7 +393,10 @@ def _process_params_dict(self, params):
355393
conv = to_mysql(conv)
356394
conv = escape(conv)
357395
conv = quote(conv)
358-
res["%({0})s".format(key).encode()] = conv
396+
if PY2:
397+
res[key] = conv
398+
else:
399+
res[key.encode()] = conv
359400
except Exception as err:
360401
raise errors.ProgrammingError(
361402
"Failed processing pyformat-parameters; %s" % err)
@@ -488,8 +529,8 @@ def execute(self, operation, params=None, multi=False):
488529

489530
if params is not None:
490531
if isinstance(params, dict):
491-
for key, value in self._process_params_dict(params).items():
492-
stmt = stmt.replace(key, value)
532+
stmt = _bytestr_format_dict(
533+
stmt, self._process_params_dict(params))
493534
elif isinstance(params, (list, tuple)):
494535
psub = _ParamSubstitutor(self._process_params(params))
495536
stmt = RE_PY_PARAM.sub(psub, stmt)
@@ -543,8 +584,8 @@ def remove_comments(match):
543584
for params in seq_params:
544585
tmp = fmt
545586
if isinstance(params, dict):
546-
for key, value in self._process_params_dict(params).items():
547-
tmp = tmp.replace(key, value)
587+
tmp = _bytestr_format_dict(
588+
tmp, self._process_params_dict(params))
548589
else:
549590
psub = _ParamSubstitutor(self._process_params(params))
550591
tmp = RE_PY_PARAM.sub(psub, tmp)

tests/test_cursor.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
22

33
# MySQL Connector/Python is licensed under the terms of the GPLv2
44
# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
@@ -360,6 +360,8 @@ def test__process_params(self):
360360
datetime.time(20, 3, 23),
361361
st_now,
362362
datetime.timedelta(hours=40, minutes=30, seconds=12),
363+
'foo %(t)s',
364+
'foo %(s)s',
363365
)
364366
exp = (
365367
b'NULL',
@@ -381,6 +383,8 @@ def test__process_params(self):
381383
b"'" + time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii')
382384
+ b"'",
383385
b"'40:30:12'",
386+
b"'foo %(t)s'",
387+
b"'foo %(s)s'",
384388
)
385389

386390
self.cnx = connection.MySQLConnection(**tests.get_mysql_config())
@@ -420,28 +424,32 @@ def test__process_params_dict(self):
420424
'p': datetime.time(20, 3, 23),
421425
'q': st_now,
422426
'r': datetime.timedelta(hours=40, minutes=30, seconds=12),
427+
's': 'foo %(t)s',
428+
't': 'foo %(s)s',
423429
}
424430
exp = {
425-
b'%(a)s': b'NULL',
426-
b'%(b)s': b'128',
427-
b'%(c)s': b'1281288',
428-
b'%(d)s': repr(float(3.14)) if PY2 else b'3.14',
429-
b'%(e)s': b"'3.14'",
430-
b'%(f)s': b"'back\\\\slash'",
431-
b'%(g)s': b"'newline\\n'",
432-
b'%(h)s': b"'return\\r'",
433-
b'%(i)s': b"'\\'single\\''",
434-
b'%(j)s': b'\'\\"double\\"\'',
435-
b'%(k)s': b"'windows\\\x1a'",
436-
b'%(l)s': b"'Strings are sexy'",
437-
b'%(m)s': b"'\xe8\x8a\xb1'",
438-
b'%(n)s': b"'2008-05-07 20:01:23'",
439-
b'%(o)s': b"'2008-05-07'",
440-
b'%(p)s': b"'20:03:23'",
441-
b'%(q)s': b"'" +
431+
b'a': b'NULL',
432+
b'b': b'128',
433+
b'c': b'1281288',
434+
b'd': repr(float(3.14)) if PY2 else b'3.14',
435+
b'e': b"'3.14'",
436+
b'f': b"'back\\\\slash'",
437+
b'g': b"'newline\\n'",
438+
b'h': b"'return\\r'",
439+
b'i': b"'\\'single\\''",
440+
b'j': b'\'\\"double\\"\'',
441+
b'k': b"'windows\\\x1a'",
442+
b'l': b"'Strings are sexy'",
443+
b'm': b"'\xe8\x8a\xb1'",
444+
b'n': b"'2008-05-07 20:01:23'",
445+
b'o': b"'2008-05-07'",
446+
b'p': b"'20:03:23'",
447+
b'q': b"'" +
442448
time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii')
443449
+ b"'",
444-
b'%(r)s': b"'40:30:12'",
450+
b'r': b"'40:30:12'",
451+
b's': b"'foo %(t)s'",
452+
b't': b"'foo %(s)s'",
445453
}
446454

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

0 commit comments

Comments
 (0)