Skip to content

Commit

Permalink
Merge pull request #1109 from davidism/max-cookie-size
Browse files Browse the repository at this point in the history
Allow configuring or disabling max cookie size check
  • Loading branch information
davidism committed Apr 18, 2017
2 parents f6564aa + a5d9477 commit fbfa21e
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 31 deletions.
7 changes: 7 additions & 0 deletions CHANGES
Expand Up @@ -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
--------------
Expand Down
21 changes: 14 additions & 7 deletions tests/test_http.py
Expand Up @@ -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):
Expand Down
41 changes: 26 additions & 15 deletions werkzeug/http.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
<cookie_>`_. Set to 0 to disable this check.
.. _`cookie`: http://browsercookielimits.squawky.net/
"""
key = to_bytes(key, charset)
value = to_bytes(value, charset)
Expand Down Expand Up @@ -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

Expand Down
34 changes: 25 additions & 9 deletions werkzeug/wrappers.py
Expand Up @@ -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 <cookie_>`_. 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):
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down

0 comments on commit fbfa21e

Please sign in to comment.