From 7757ad9c9423430f13a637022fc6f85c5ed6209c Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Wed, 28 Dec 2011 19:58:57 +0100 Subject: fix: Workaround for a hash collision DoS vulnerability in CPython dicts. If the language does not provide a randomized hash function or the application server does not recognize attacks using multi-collisions, an attacker can degenerate the hash table by sending lots of colliding keys. The algorithmic complexity of inserting n elements into the table then goes to O(n**2), making it possible to exhaust hours of CPU time using a single HTTP request. This workaround limits the number of GET, POST and cookie parameters to a reasonable maximum of 100 key/value pairs per request, reducing the effectiveness of such attacks. Normal web applications should not need to process more than 100 parameters per request, but this limit can be changed by setting Request.MAX_PARAMS to a different value. Some links: https://cryptanalysis.eu/blog/2011/12/28/effective-dos-attacks-against-web-application-plattforms-hashdos/ http://events.ccc.de/congress/2011/Fahrplan/events/4680.en.html http://www.nruns.com/_downloads/advisory28122011.pdf --- bottle.py | 18 ++++++++++-------- test/test_environ.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/bottle.py b/bottle.py index c530838..a107474 100755 --- a/bottle.py +++ b/bottle.py @@ -70,9 +70,9 @@ try: from collections import MutableMapping as DictMixin except ImportError: # pragma: no cover from UserDict import DictMixin -try: from urlparse import parse_qs +try: from urlparse import parse_qsl except ImportError: # pragma: no cover - from cgi import parse_qs + from cgi import parse_qsl try: import cPickle as pickle except ImportError: # pragma: no cover @@ -864,6 +864,8 @@ class BaseRequest(DictMixin): #: Maximum size of memory buffer for :attr:`body` in bytes. MEMFILE_MAX = 102400 + #: Maximum number pr GET or POST parameters per request + MAX_PARAMS = 100 def __init__(self, environ): """ Wrap a WSGI environ dictionary. """ @@ -898,7 +900,8 @@ class BaseRequest(DictMixin): """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT decoded. Use :meth:`get_cookie` if you expect signed cookies. """ cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')) - return FormsDict((c.key, c.value) for c in cookies.itervalues()) + cookies = list(cookies.values())[:self.MAX_PARAMS] + return FormsDict((c.key, c.value) for c in cookies) def get_cookie(self, key, default=None, secret=None): """ Return the content of a cookie. To read a `Signed Cookie`, the @@ -917,11 +920,10 @@ class BaseRequest(DictMixin): values are sometimes called "URL arguments" or "GET parameters", but not to be confused with "URL wildcards" as they are provided by the :class:`Router`. ''' - data = parse_qs(self.query_string, keep_blank_values=True) + pairs = parse_qsl(self.query_string, keep_blank_values=True) get = self.environ['bottle.get'] = FormsDict() - for key, values in data.iteritems(): - for value in values: - get[key] = value + for key, value in pairs[:self.MAX_PARAMS]: + get[key] = value return get @DictProperty('environ', 'bottle.request.forms', read_only=True) @@ -1023,7 +1025,7 @@ class BaseRequest(DictMixin): else: fb = self.body data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) - for item in data.list or []: + for item in (data.list or [])[:self.MAX_PARAMS]: post[item.name] = item if item.filename else item.value return post diff --git a/test/test_environ.py b/test/test_environ.py index 81ca695..f54db62 100755 --- a/test/test_environ.py +++ b/test/test_environ.py @@ -366,6 +366,23 @@ class TestRequest(unittest.TestCase): del r.environ['HTTP_X_FORWARDED_FOR'] self.assertEqual(r.remote_addr, ips[1]) + def test_maxparam(self): + ips = ['1.2.3.4', '2.3.4.5', '3.4.5.6'] + e = {} + wsgiref.util.setup_testing_defaults(e) + e['wsgi.input'].write(tob('a=a&b=b&c=c')) + e['wsgi.input'].seek(0) + e['CONTENT_LENGTH'] = '11' + e['REQUEST_METHOD'] = "POST" + e['HTTP_COOKIE'] = 'a=1,b=1,c=1;d=1' + e['QUERY_STRING'] = 'a&b&c&d' + r = BaseRequest(e) + r.MAX_PARAMS = 2 + self.assertEqual(len(list(r.query.allitems())), 2) + self.assertEqual(len(list(r.cookies.allitems())), 2) + self.assertEqual(len(list(r.forms.allitems())), 2) + self.assertEqual(len(list(r.params.allitems())), 4) + class TestResponse(unittest.TestCase): -- cgit v1.2.1