From e02b1f7b8e2afa1efe0a38e1809f2fce76df3f24 Mon Sep 17 00:00:00 2001 From: Jason G <41053218+gianghungtien@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:01:38 +0700 Subject: [PATCH] gh-154546: Make http.cookies.BaseCookie.load() atomic on error BaseCookie.load() parses the whole cookie string before applying it, but the key and attribute legality checks run during the apply phase (inside Morsel.set() and Morsel.__setitem__()). An illegal key or unknown $-prefixed attribute part-way through the string therefore raised CookieError only after the earlier cookies had already been written to the jar, leaving it in a partially-updated state. Build the morsels in a temporary mapping and only commit them to the cookie jar once the entire string has been validated, so load() is now all-or-nothing. Existing morsels are copied before being updated, so a failing load() no longer mutates cookies that were already present. --- Lib/http/cookies.py | 20 +++++++++++-- Lib/test/test_http_cookies.py | 28 +++++++++++++++++++ ...-07-25-12-00-00.gh-issue-154546.bK9xQ2.rst | 4 +++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-25-12-00-00.gh-issue-154546.bK9xQ2.rst diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py index 9a6f01dfb5e69a..2ebc5bccf0d6a7 100644 --- a/Lib/http/cookies.py +++ b/Lib/http/cookies.py @@ -624,7 +624,11 @@ def __parse_string(self, str, patt=_CookiePattern): # Invalid cookie string return - # The cookie string is valid, apply it. + # The cookie string parsed cleanly. Build the morsels in a temporary + # mapping and only mutate self once every key and attribute has been + # validated, so that an illegal key or attribute later in the string + # cannot leave the cookie jar partially updated (gh-154546). + built = {} # key -> Morsel, staged until the whole string is valid M = None # current morsel for tp, key, value in parsed_items: if tp == TYPE_ATTRIBUTE: @@ -633,8 +637,18 @@ def __parse_string(self, str, patt=_CookiePattern): else: assert tp == TYPE_KEYVALUE rval, cval = value - self.__set(key, rval, cval) - M = self[key] + if key in built: + M = built[key] + elif key in self: + # Preserve attributes already set on an existing morsel, + # but work on a copy so self stays untouched until commit. + M = self[key].copy() + else: + M = Morsel() + M.set(key, rval, cval) + built[key] = M + for key, M in built.items(): + dict.__setitem__(self, key, M) class SimpleCookie(BaseCookie): diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py index d1df2ec42f0d14..f945d0f26f1a96 100644 --- a/Lib/test/test_http_cookies.py +++ b/Lib/test/test_http_cookies.py @@ -320,6 +320,34 @@ def test_illegal_chars(self): with self.assertRaises(cookies.CookieError): C.load(rawdata) + def test_load_does_not_apply_partial_state_on_error(self): + # BaseCookie.load() is all-or-nothing: if the raw string contains an + # illegal key or attribute that raises CookieError, none of the + # cookies from that string are applied to the jar (gh-154546). + for rawdata in ( + "a=1; b,c=2; d=3", # illegal key in the middle + "good=1; bad,key=2", # illegal key at the end + "a=1; $foo=2", # unknown $-prefixed attribute + ): + with self.subTest(rawdata=rawdata): + C = cookies.SimpleCookie() + with self.assertRaises(cookies.CookieError): + C.load(rawdata) + self.assertEqual(dict(C), {}) + self.assertEqual(C.output(), '') + + def test_load_error_leaves_existing_cookies_untouched(self): + # A load() that raises CookieError must not mutate cookies that were + # already present in the jar (gh-154546). + C = cookies.SimpleCookie() + C['existing'] = 'kept' + C['existing']['path'] = '/here' + with self.assertRaises(cookies.CookieError): + C.load("new=1; bad,key=2") + self.assertEqual(list(C), ['existing']) + self.assertEqual(C['existing'].value, 'kept') + self.assertEqual(C['existing']['path'], '/here') + def test_comment_quoting(self): c = cookies.SimpleCookie() c['foo'] = '\N{COPYRIGHT SIGN}' diff --git a/Misc/NEWS.d/next/Library/2026-07-25-12-00-00.gh-issue-154546.bK9xQ2.rst b/Misc/NEWS.d/next/Library/2026-07-25-12-00-00.gh-issue-154546.bK9xQ2.rst new file mode 100644 index 00000000000000..a9f1ef7fc31aaa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-25-12-00-00.gh-issue-154546.bK9xQ2.rst @@ -0,0 +1,4 @@ +:meth:`http.cookies.BaseCookie.load` is now atomic: if the cookie string +contains an illegal key or attribute that raises +:exc:`~http.cookies.CookieError`, the cookie jar is left unchanged instead of +being partially updated with the cookies that preceded the error.