diff --git a/CHANGES b/CHANGES index a31de0e1c..af1c65ac5 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,13 @@ yet to be released using the converter. (``#1102``) - ``Authorization.qop`` is a string instead of a set, to comply with RFC 2617. (``#984``) +- An exception is raised when an encoded cookie is larger than, by default, + 4093 bytes. Browsers may silently ignore cookies larger than this. + ``BaseResponse`` has a new attribute ``max_cookie_size`` and ``dump_cookie`` + has a new argument ``max_size`` to configure this. (`#780`_, `#1109`_) + +.. _`#780`: https://github.com/pallets/werkzeug/pull/780 +.. _`#1109`: https://github.com/pallets/werkzeug/pull/1109 Version 0.12.1 -------------- diff --git a/tests/test_http.py b/tests/test_http.py index 63c51a66e..1fe992959 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -417,13 +417,20 @@ def test_cookie_domain_encoding(self): val = http.dump_cookie('foo', 'bar', domain=u'.foo.com') strict_eq(val, 'foo=bar; Domain=.foo.com; Path=/') - def test_cookie_maxsize(self): - val = http.dump_cookie('foo', ('bar' * 1360) + 'b') - assert len(val) == http.COOKIE_MAXSIZE - - with pytest.raises(ValueError) as excinfo: - http.dump_cookie('foo', ('bar' * 1360) + 'ba') - assert ('Cookie too large' in str(excinfo)) + def test_cookie_maxsize(self, recwarn): + val = http.dump_cookie('foo', 'bar' * 1360 + 'b') + assert len(recwarn) == 0 + assert len(val) == 4093 + + http.dump_cookie('foo', 'bar' * 1360 + 'ba') + assert len(recwarn) == 1 + w = recwarn.pop() + assert 'cookie is too large' in str(w.message) + + http.dump_cookie('foo', b'w' * 502, max_size=512) + assert len(recwarn) == 1 + w = recwarn.pop() + assert 'the limit is 512 bytes' in str(w.message) class TestRange(object): diff --git a/werkzeug/http.py b/werkzeug/http.py index 58451f56d..ee8b9d35f 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -17,6 +17,7 @@ :license: BSD, see LICENSE for more details. """ import re +import warnings from time import time, gmtime try: from email.utils import parsedate_tz @@ -139,11 +140,6 @@ 510: 'Not Extended' } -# For discussion of a safe (i.e. lowest common denominator) cookie -# max size, see: -# http://browsercookielimits.squawky.net/ -COOKIE_MAXSIZE = 4093 - def wsgi_to_bytes(data): """coerce wsgi unicode represented bytes to real ones @@ -985,7 +981,7 @@ def _parse_pairs(): def dump_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, - charset='utf-8', sync_expires=True): + charset='utf-8', sync_expires=True, max_size=4093): """Creates a new Set-Cookie header without the ``Set-Cookie`` prefix The parameters are the same as in the cookie Morsel object in the Python standard library but it accepts unicode data, too. @@ -1021,6 +1017,11 @@ def dump_cookie(key, value='', max_age=None, expires=None, path='/', :param charset: the encoding for unicode values. :param sync_expires: automatically set expires if max_age is defined but expires not. + :param max_size: Warn if the final header value exceeds this size. The + default, 4093, should be safely `supported by most browsers + `_. Set to 0 to disable this check. + + .. _`cookie`: http://browsercookielimits.squawky.net/ """ key = to_bytes(key, charset) value = to_bytes(value, charset) @@ -1070,16 +1071,26 @@ def dump_cookie(key, value='', max_age=None, expires=None, path='/', if not PY2: rv = rv.decode('latin1') - # Check that the final value of the cookie is less than the - # standard limit set by browsers. If no check is performed, and if - # the cookie is too large, then it will simply get lost, which can - # be quite hard to debug. + # Warn if the final value of the cookie is less than the limit. If the + # cookie is too large, then it may be silently ignored, which can be quite + # hard to debug. cookie_size = len(rv) - if cookie_size > COOKIE_MAXSIZE: - raise ValueError(( - 'Cookie too large: size of {0} is {1} bytes, ' - 'standard limit in most browsers is {2} bytes').format( - key, cookie_size, COOKIE_MAXSIZE)) + + if max_size and cookie_size > max_size: + value_size = len(value) + warnings.warn( + 'The "{key}" cookie is too large: the value was {value_size} bytes' + ' but the header required {extra_size} extra bytes. The final size' + ' was {cookie_size} bytes but the limit is {max_size} bytes.' + ' Browsers may silently ignore cookies larger than this.'.format( + key=key, + value_size=value_size, + extra_size=cookie_size - value_size, + cookie_size=cookie_size, + max_size=max_size + ), + stacklevel=2 + ) return rv diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index 49cebd69b..2451a35b7 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -804,6 +804,16 @@ def application(environ, start_response): #: .. versionadded:: 0.8 automatically_set_content_length = True + #: Warn if a cookie header exceeds this size. The default, 4093, should be + #: safely `supported by most browsers `_. A cookie larger than + #: this size will still be sent, but it may be ignored or handled + #: incorrectly by some browsers. Set to 0 to disable this check. + #: + #: .. versionadded:: 0.13 + #: + #: .. _`cookie`: http://browsercookielimits.squawky.net/ + max_cookie_size = 4093 + def __init__(self, response=None, status=None, headers=None, mimetype=None, content_type=None, direct_passthrough=False): if isinstance(headers, Headers): @@ -1054,6 +1064,9 @@ def set_cookie(self, key, value='', max_age=None, expires=None, """Sets a cookie. The parameters are the same as in the cookie `Morsel` object in the Python standard library but it accepts unicode data, too. + A warning is raised if the size of the cookie header exceeds + :attr:`max_cookie_size`, but the header will still be set. + :param key: the key (name) of the cookie to be set. :param value: the value of the cookie. :param max_age: should be a number of seconds, or `None` (default) if @@ -1072,15 +1085,18 @@ def set_cookie(self, key, value='', max_age=None, expires=None, extension to the cookie standard and probably not supported by all browsers. """ - self.headers.add('Set-Cookie', dump_cookie(key, - value=value, - max_age=max_age, - expires=expires, - path=path, - domain=domain, - secure=secure, - httponly=httponly, - charset=self.charset)) + self.headers.add('Set-Cookie', dump_cookie( + key, + value=value, + max_age=max_age, + expires=expires, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + charset=self.charset, + max_size=self.max_cookie_size + )) def delete_cookie(self, key, path='/', domain=None): """Delete a cookie. Fails silently if key doesn't exist.