Hi, On Sat, Oct 22, 2016 at 08:27:56AM +0200, Salvatore Bonaccorso wrote: > Source: mysql-connector-python > Version: 2.1.3-1 > Severity: grave > Tags: security upstream > > Hi, > > the following vulnerability was published for mysql-connector-python. > > CVE-2016-5598[0]. > > If you fix the vulnerability please also make sure to include the > CVE (Common Vulnerabilities & Exposures) id in your changelog entry. > > For further information see: > > [0] https://security-tracker.debian.org/tracker/CVE-2016-5598 > [1] > http://www.oracle.com/technetwork/security-advisory/cpuoct2016verbose-2881725.html#MSQL > > Please adjust the affected versions in the BTS as needed.
While 2.1.4 has other changes the only change in 2.0.5 is the CVE fix. It seems this is caused by format string expansion in _format_params_dict. I've attached the diff between 2.0.4 → 2.0.5. I think wheezy is affected since it uses pythons format expansion there but I'd be glad about a second opinion. Cheers, -- Guido
diff --git a/CHANGES.txt b/CHANGES.txt index 7ee20de..fbdca18 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,11 +3,16 @@ MySQL Connector/Python 2.0 - Release Notes & Changes ==================================================== MySQL Connector/Python -Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. Full release notes: http://dev.mysql.com/doc/relnotes/connector-python/en/ + +v2.0.5 +====== +- BUG22529828: Fix potencial SQL injection + v2.0.4 ====== diff --git a/PKG-INFO b/PKG-INFO index de8b3a5..46adcb2 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: mysql-connector-python -Version: 2.0.4 +Version: 2.0.5 Summary: MySQL driver written in Python Home-page: http://dev.mysql.com/doc/connector-python/en/index.html Author: Oracle and/or its affiliates diff --git a/README.txt b/README.txt index 2f88034..99be628 100644 --- a/README.txt +++ b/README.txt @@ -3,7 +3,7 @@ MySQL Connector/Python 2.0 ========================== MySQL Connector/Python -Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. License information can be found in the LICENSE.txt file. @@ -28,7 +28,7 @@ doubt, this particular copy of the software is released under the version 2 of the GNU General Public License. MySQL Connector/Python is brought to you by Oracle. -Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. License information can be found in the LICENSE.txt file. diff --git a/lib/mysql/connector/cursor.py b/lib/mysql/connector/cursor.py index 3e6a2b4..b9bfd9a 100644 --- a/lib/mysql/connector/cursor.py +++ b/lib/mysql/connector/cursor.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most @@ -29,6 +29,7 @@ import re import weakref from . import errors +from .catch23 import PY2 SQL_COMMENT = r"\/\*.*?\*\/" RE_SQL_COMMENT = re.compile( @@ -42,6 +43,14 @@ RE_SQL_INSERT_STMT = re.compile( re.I | re.M | re.S) RE_SQL_INSERT_VALUES = re.compile(r'.*VALUES\s*(\(.*\)).*', re.I | re.M | re.S) RE_PY_PARAM = re.compile(b'(%s)') +RE_PY_MAPPING_PARAM = re.compile( + br''' + % + \((?P<mapping_key>[^)]+)\) + (?P<conversion_type>[diouxXeEfFgGcrs%]) + ''', + re.X +) RE_SQL_SPLIT_STMTS = re.compile( b''';(?=(?:[^"'`]*["'`][^"'`]*["'`])*[^"'`]*$)''') RE_SQL_FIND_PARAM = re.compile( @@ -73,6 +82,35 @@ class _ParamSubstitutor(object): return len(self.params) - self.index +def _bytestr_format_dict(bytestr, value_dict): + """ + >>> _bytestr_format_dict(b'%(a)s', {b'a': b'foobar'}) + b'foobar + >>> _bytestr_format_dict(b'%%(a)s', {b'a': b'foobar'}) + b'%%(a)s' + >>> _bytestr_format_dict(b'%%%(a)s', {b'a': b'foobar'}) + b'%%foobar' + >>> _bytestr_format_dict(b'%(x)s %(y)s', + ... {b'x': b'x=%(y)s', b'y': b'y=%(x)s'}) + b'x=%(y)s y=%(x)s' + """ + def replace(matchobj): + value = None + groups = matchobj.groupdict() + if groups["conversion_type"] == b"%": + value = b"%" + if groups["conversion_type"] == b"s": + key = groups["mapping_key"].encode("utf-8") \ + if PY2 else groups["mapping_key"] + value = value_dict[key] + if value is None: + raise ValueError("Unsupported conversion_type: {0}" + "".format(groups["conversion_type"])) + return value.decode("utf-8") if PY2 else value + return RE_PY_MAPPING_PARAM.sub(replace, bytestr.decode("utf-8") + if PY2 else bytestr) + + class CursorBase(object): """ Base for defining MySQLCursor. This class is a skeleton and defines @@ -355,7 +393,10 @@ class MySQLCursor(CursorBase): conv = to_mysql(conv) conv = escape(conv) conv = quote(conv) - res["%({0})s".format(key).encode()] = conv + if PY2: + res[key] = conv + else: + res[key.encode()] = conv except Exception as err: raise errors.ProgrammingError( "Failed processing pyformat-parameters; %s" % err) @@ -488,8 +529,8 @@ class MySQLCursor(CursorBase): if params is not None: if isinstance(params, dict): - for key, value in self._process_params_dict(params).items(): - stmt = stmt.replace(key, value) + stmt = _bytestr_format_dict( + stmt, self._process_params_dict(params)) elif isinstance(params, (list, tuple)): psub = _ParamSubstitutor(self._process_params(params)) stmt = RE_PY_PARAM.sub(psub, stmt) @@ -543,8 +584,8 @@ class MySQLCursor(CursorBase): for params in seq_params: tmp = fmt if isinstance(params, dict): - for key, value in self._process_params_dict(params).items(): - tmp = tmp.replace(key, value) + tmp = _bytestr_format_dict( + tmp, self._process_params_dict(params)) else: psub = _ParamSubstitutor(self._process_params(params)) tmp = RE_PY_PARAM.sub(psub, tmp) diff --git a/lib/mysql/connector/version.py b/lib/mysql/connector/version.py index 99267f2..9431b1b 100644 --- a/lib/mysql/connector/version.py +++ b/lib/mysql/connector/version.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most @@ -26,7 +26,7 @@ The file version.py gets installed and is available after installation as mysql.connector.version. """ -VERSION = (2, 0, 4, '', 0) +VERSION = (2, 0, 5, '', 0) if VERSION[3] and VERSION[4]: VERSION_TEXT = '{0}.{1}.{2}{3}{4}'.format(*VERSION) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index e07342c..e2aef9f 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most @@ -360,6 +360,8 @@ class MySQLCursorTests(tests.TestsCursor): datetime.time(20, 3, 23), st_now, datetime.timedelta(hours=40, minutes=30, seconds=12), + 'foo %(t)s', + 'foo %(s)s', ) exp = ( b'NULL', @@ -381,6 +383,8 @@ class MySQLCursorTests(tests.TestsCursor): b"'" + time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii') + b"'", b"'40:30:12'", + b"'foo %(t)s'", + b"'foo %(s)s'", ) self.cnx = connection.MySQLConnection(**tests.get_mysql_config()) @@ -420,28 +424,32 @@ class MySQLCursorTests(tests.TestsCursor): 'p': datetime.time(20, 3, 23), 'q': st_now, 'r': datetime.timedelta(hours=40, minutes=30, seconds=12), + 's': 'foo %(t)s', + 't': 'foo %(s)s', } exp = { - b'%(a)s': b'NULL', - b'%(b)s': b'128', - b'%(c)s': b'1281288', - b'%(d)s': repr(float(3.14)) if PY2 else b'3.14', - b'%(e)s': b"'3.14'", - b'%(f)s': b"'back\\\\slash'", - b'%(g)s': b"'newline\\n'", - b'%(h)s': b"'return\\r'", - b'%(i)s': b"'\\'single\\''", - b'%(j)s': b'\'\\"double\\"\'', - b'%(k)s': b"'windows\\\x1a'", - b'%(l)s': b"'Strings are sexy'", - b'%(m)s': b"'\xe8\x8a\xb1'", - b'%(n)s': b"'2008-05-07 20:01:23'", - b'%(o)s': b"'2008-05-07'", - b'%(p)s': b"'20:03:23'", - b'%(q)s': b"'" + + b'a': b'NULL', + b'b': b'128', + b'c': b'1281288', + b'd': repr(float(3.14)) if PY2 else b'3.14', + b'e': b"'3.14'", + b'f': b"'back\\\\slash'", + b'g': b"'newline\\n'", + b'h': b"'return\\r'", + b'i': b"'\\'single\\''", + b'j': b'\'\\"double\\"\'', + b'k': b"'windows\\\x1a'", + b'l': b"'Strings are sexy'", + b'm': b"'\xe8\x8a\xb1'", + b'n': b"'2008-05-07 20:01:23'", + b'o': b"'2008-05-07'", + b'p': b"'20:03:23'", + b'q': b"'" + time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii') + b"'", - b'%(r)s': b"'40:30:12'", + b'r': b"'40:30:12'", + b's': b"'foo %(t)s'", + b't': b"'foo %(s)s'", } self.cnx = connection.MySQLConnection(**tests.get_mysql_config())