diff --git a/Doc/library/xmlrpclib.rst b/Doc/library/xmlrpclib.rst --- a/Doc/library/xmlrpclib.rst +++ b/Doc/library/xmlrpclib.rst @@ -120,6 +120,15 @@ *__dict__* attribute and don't have a base class that is marshalled in a special way. +.. data:: MAX_GZIP_DECODE + + The module constant specifies the amount of bytes that are decompressed by + :func:`gzip_decode`. The default value is *20 MB*. A value of *-1* disables + the protection. + + .. versionadded:: 2.7.4 + The constant was added to strengthen the module against gzip bomb + attacks. .. seealso:: diff --git a/Lib/test/test_xmlrpc.py b/Lib/test/test_xmlrpc.py --- a/Lib/test/test_xmlrpc.py +++ b/Lib/test/test_xmlrpc.py @@ -19,6 +19,11 @@ threading = None try: + import gzip +except ImportError: + gzip = None + +try: unicode except NameError: have_unicode = False @@ -731,7 +736,7 @@ with cm: p.pow(6, 8) - def test_gsip_response(self): + def test_gzip_response(self): t = self.Transport() p = xmlrpclib.ServerProxy(URL, transport=t) old = self.requestHandler.encode_threshold @@ -744,6 +749,27 @@ self.requestHandler.encode_threshold = old self.assertTrue(a>b) + def test_gzip_decode_limit(self): + data = '\0' * xmlrpclib.MAX_GZIP_DECODE + encoded = xmlrpclib.gzip_encode(data) + decoded = xmlrpclib.gzip_decode(encoded) + self.assertEqual(len(decoded), xmlrpclib.MAX_GZIP_DECODE) + + data = '\0' * (xmlrpclib.MAX_GZIP_DECODE + 1) + encoded = xmlrpclib.gzip_encode(data) + + with self.assertRaisesRegexp(ValueError, + "max gzipped payload length exceeded"): + xmlrpclib.gzip_decode(encoded) + + oldmax = xmlrpclib.MAX_GZIP_DECODE + try: + xmlrpclib.MAX_GZIP_DECODE = -1 + xmlrpclib.gzip_decode(encoded) + finally: + xmlrpclib.MAX_GZIP_DECODE = oldmax + + #Test special attributes of the ServerProxy object class ServerProxyTestCase(unittest.TestCase): def setUp(self): @@ -1011,11 +1037,8 @@ xmlrpc_tests.append(SimpleServerTestCase) xmlrpc_tests.append(KeepaliveServerTestCase1) xmlrpc_tests.append(KeepaliveServerTestCase2) - try: - import gzip + if gzip is not None: xmlrpc_tests.append(GzipServerTestCase) - except ImportError: - pass #gzip not supported in this build xmlrpc_tests.append(MultiPathServerTestCase) xmlrpc_tests.append(ServerProxyTestCase) xmlrpc_tests.append(FailingServerTestCase) diff --git a/Lib/xmlrpclib.py b/Lib/xmlrpclib.py --- a/Lib/xmlrpclib.py +++ b/Lib/xmlrpclib.py @@ -49,6 +49,7 @@ # 2003-07-12 gp Correct marshalling of Faults # 2003-10-31 mvl Add multicall support # 2004-08-20 mvl Bump minimum supported Python version to 2.1 +# 2013-01-20 ch Add workaround for gzip bomb vulnerability # # Copyright (c) 1999-2002 by Secret Labs AB. # Copyright (c) 1999-2002 by Fredrik Lundh. @@ -147,6 +148,10 @@ except ImportError: gzip = None #python can be built without zlib/gzip support +# Limit the maximum amount of decoded data that is decompressed. The +# limit prevents gzip bomb attacks. +MAX_GZIP_DECODE = 20 * 1024 * 1024 # 20 MB + # -------------------------------------------------------------------- # Internal stuff @@ -1178,11 +1183,16 @@ f = StringIO.StringIO(data) gzf = gzip.GzipFile(mode="rb", fileobj=f) try: - decoded = gzf.read() + if MAX_GZIP_DECODE < 0: # no limit + decoded = gzf.read() + else: + decoded = gzf.read(MAX_GZIP_DECODE + 1) except IOError: raise ValueError("invalid data") f.close() gzf.close() + if MAX_GZIP_DECODE >= 0 and len(decoded) > MAX_GZIP_DECODE: + raise ValueError("max gzipped payload length exceeded") return decoded ##