diff options
author | Marcel Hellkamp <marc@gsites.de> | 2011-12-28 19:58:57 +0100 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2011-12-28 20:15:59 +0100 |
commit | 7757ad9c9423430f13a637022fc6f85c5ed6209c (patch) | |
tree | 7dbfd5bfeed128b3d06cccb3d3db719ef22799d6 | |
parent | c797b339e9859d1704c4611642c0a9d9e0672b62 (diff) | |
download | bottle-7757ad9c9423430f13a637022fc6f85c5ed6209c.tar.gz |
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
-rwxr-xr-x | bottle.py | 18 | ||||
-rwxr-xr-x | test/test_environ.py | 17 |
2 files changed, 27 insertions, 8 deletions
@@ -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): |