Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Doc/library/http.cookies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ in a cookie name (as :attr:`~Morsel.key`).
.. versionchanged:: 3.3
Allowed '``:``' as a valid cookie name character.

.. versionchanged:: 3.15
Allowed '``"``' as a valid cookie value character.

.. note::

On encountering an invalid cookie, :exc:`CookieError` is raised, so if your
Expand Down Expand Up @@ -118,6 +115,11 @@ Cookie Objects
for k, v in rawdata.items():
cookie[k] = v

.. versionchanged:: next
Allowed any characters except semicolon (``';'``) and control
characters in non-quoted cookie values.
The ``';'`` separator is now mandatory between name-value pairs.


.. _morsel-objects:

Expand Down Expand Up @@ -315,8 +317,8 @@ The following example demonstrates how to use the :mod:`!http.cookies` module.
Set-Cookie: string=seven
>>> import json
>>> C = cookies.SimpleCookie()
>>> C.load(f'cookies=7; mixins="{json.dumps({"chips": "dark chocolate"})}"; state=gooey')
>>> C.load(f'cookies=7; mixins={json.dumps({"chips": "dark chocolate"})}; state=gooey')
>>> print(C)
Set-Cookie: cookies=7
Set-Cookie: mixins="{"chips": "dark chocolate"}"
Set-Cookie: mixins={"chips": "dark chocolate"}
Set-Cookie: state=gooey
6 changes: 4 additions & 2 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -941,8 +941,10 @@ http.client
http.cookies
------------

* Allow '``"``' double quotes in cookie values.
(Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.)
* Cookie parsing now allows any characters except semicolon (``';'``)
and control characters in non-quoted cookie values.
The ``';'`` separator is now mandatory between name-value pairs.
(Contributed by Serhiy Storchaka in :gh:`149028`.)


http.server
Expand Down
48 changes: 24 additions & 24 deletions Lib/http/cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,30 +450,23 @@ def OutputString(self, attrs=None):
# specifications. I have since discovered that MSIE 3.0x doesn't
# follow the character rules outlined in those specs. As a
# result, the parsing rules here are less strict.
#
# Currently, it is a hybrid of RFC 2109/2965 (for quoted strings)
# and RFC 6265.

_LegalKeyChars = r"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\="
_LegalValueChars = _LegalKeyChars + r'\[\]'
_CookiePattern = re.compile(r"""
\s* # Optional whitespace at start of cookie
(?P<key> # Start of group 'key'
[""" + _LegalKeyChars + r"""]+? # Any word of at least one letter
) # End of group 'key'
( # Optional group: there may not be a value.
\s*=\s* # Equal Sign
(?P<val> # Start of group 'val'
"(?:\\"|.)*?" # Any double-quoted string
| # or
# Special case for "expires" attr
(\w{3,6}day|\w{3}),\s # Day of the week or abbreviated day
[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Date and time in specific format
| # or
[""" + _LegalValueChars + r"""]* # Any word or empty string
) # End of group 'val'
)? # End of optional value group
\s* # Any number of spaces.
(\s+|;|$) # Ending either at space, semicolon, or EOS.
""", re.ASCII | re.VERBOSE) # re.ASCII may be removed if safe.
\s*+ # Optional whitespace at start of cookie
([^=;]*+) # Name: any characters except "=" and ";" (RFC 6265)
(?: # Optional group: there may not be a value.
\s*+=\s*+ # Equal Sign
( # Value:
"(?:\\.|[^"])*+" # Any double-quoted string (RFC 2109/2965)
| # or
[^;]*+ # Any characters except ";" (RFC 6265)
)
)?+ # End of optional value group
\s*+ # Any number of spaces.
(?:;|\z) # Ending either at semicolon, or EOS.
""", re.ASCII | re.VERBOSE) # re.ASCII is needed for \s.


# At long last, here is the cookie class. Using this class is almost just like
Expand Down Expand Up @@ -580,8 +573,15 @@ def __parse_string(self, str, patt=_CookiePattern):
# No more cookies
break

key, value = match.group("key"), match.group("val")
i = match.end(0)
key, value = match.groups()
key = key.rstrip(' \t\r\n')
if value:
value = value.rstrip(' \t\r\n')
if not _is_legal_key(key):
break
if value and _has_control_character(value):
break
i = match.end()

if key[0] == "$":
if not morsel_seen:
Expand Down
18 changes: 6 additions & 12 deletions Lib/test/test_http_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ def test_basic(self):
))
},

# gh-92936: allow double quote in cookie values
# gh-149028: allow any characters in unquoted cookie values
{
'data': 'cookie="{"key": "value"}"',
'data': 'cookie={"key": "value"}',
'dict': {'cookie': '{"key": "value"}'},
'repr': "<SimpleCookie: cookie='{\"key\": \"value\"}'>",
'output': 'Set-Cookie: cookie="{"key": "value"}"',
'output': 'Set-Cookie: cookie={"key": "value"}',
},
{
'data': 'key="some value; surrounded by quotes"',
Expand All @@ -64,11 +64,11 @@ def test_basic(self):
'output': 'Set-Cookie: key="some value; surrounded by quotes"',
},
{
'data': 'session="user123"; preferences="{"theme": "dark"}"',
'data': 'session="user123"; preferences={"theme": "dark"}',
'dict': {'session': 'user123', 'preferences': '{"theme": "dark"}'},
'repr': "<SimpleCookie: preferences='{\"theme\": \"dark\"}' session='user123'>",
'output': '\n'.join((
'Set-Cookie: preferences="{"theme": "dark"}"',
'Set-Cookie: preferences={"theme": "dark"}',
'Set-Cookie: session="user123"',
))
}
Expand Down Expand Up @@ -314,7 +314,7 @@ def test_invalid_cookies(self):
C = cookies.SimpleCookie()
for s in (']foo=x', '[foo=x', 'blah]foo=x', 'blah[foo=x',
'Set-Cookie: foo=bar', 'Set-Cookie: foo',
'foo=bar; baz', 'baz; foo=bar',
'foo=bar; baz', 'baz; foo=bar', 'foo,bar=baz',
'secure;foo=bar', 'Version=1;foo=bar'):
C.load(s)
self.assertEqual(dict(C), {})
Expand All @@ -333,12 +333,6 @@ def test_pickle(self):
C1 = pickle.loads(pickle.dumps(C, protocol=proto))
self.assertEqual(C1.output(), expected_output)

def test_illegal_chars(self):
rawdata = "a=b; c,d=e"
C = cookies.SimpleCookie()
with self.assertRaises(cookies.CookieError):
C.load(rawdata)

def test_comment_quoting(self):
c = cookies.SimpleCookie()
c['foo'] = '\N{COPYRIGHT SIGN}'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Relax parsing :mod:`HTTP coockies <http.cookies>` values: allow any
characters except semicolon (``';'``) and control characters in non-quoted
cookie values. The ``';'`` separator is now mandatory between name-value pairs.
Loading