summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2011-12-28 19:58:57 +0100
committerMarcel Hellkamp <marc@gsites.de>2011-12-28 20:15:59 +0100
commit7757ad9c9423430f13a637022fc6f85c5ed6209c (patch)
tree7dbfd5bfeed128b3d06cccb3d3db719ef22799d6
parentc797b339e9859d1704c4611642c0a9d9e0672b62 (diff)
downloadbottle-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-xbottle.py18
-rwxr-xr-xtest/test_environ.py17
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):