https://github.com/python/cpython/commit/4e9d037f1cfc8e29efcb1f67bc668e06ff2b6643
commit: 4e9d037f1cfc8e29efcb1f67bc668e06ff2b6643
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: Eclips4 <[email protected]>
date: 2026-11-07T23:56:34+02:00
summary:

[3.15] gh-161569: Check cookie policy before deleting expired cookies in 
`http.cookiejar` (GH-161570) (#161579)

gh-161569: Check cookie policy before deleting expired cookies in 
`http.cookiejar` (GH-161570)

gh-161569: Check cookie policy before deleting expired cookies in http.cookiejar

An expired Set-Cookie header was allowed to delete a stored cookie using
the Domain and Path attributes taken verbatim from the header, before
CookiePolicy.set_ok() was consulted. A response from one host could
therefore delete cookies belonging to any other domain in a shared jar.

The policy is now consulted before an expired cookie may delete an
existing one, as required by RFC 6265 section 5.3.
(cherry picked from commit 8eb3d2c1c41ca7ce628554413981c1eb763a1494)

Co-authored-by: Kirill Podoprigora <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A Misc/NEWS.d/next/Security/2026-11-07-01-43-49.gh-issue-161569.cookiej.rst
M Lib/http/cookiejar.py
M Lib/test/test_http_cookiejar.py

diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py
index 1da4774b469ce74..913091e0976c5e5 100644
--- a/Lib/http/cookiejar.py
+++ b/Lib/http/cookiejar.py
@@ -1568,28 +1568,33 @@ def _cookie_from_cookie_tuple(self, tup, request):
         if expires is Absent:
             expires = None
             discard = True
-        elif expires <= self._now:
-            # Expiry date in past is request to delete cookie.  This can't be
-            # in DefaultCookiePolicy, because can't delete cookies there.
-            try:
-                self.clear(domain, path, name)
-            except KeyError:
-                pass
-            _debug("Expiring cookie, domain='%s', path='%s', name='%s'",
-                   domain, path, name)
+
+        cookie = Cookie(version,
+                        name, value,
+                        port, port_specified,
+                        domain, domain_specified, domain_initial_dot,
+                        path, path_specified,
+                        secure,
+                        expires,
+                        discard,
+                        comment,
+                        comment_url,
+                        rest)
+
+        if expires is not None and expires <= self._now:
+            # RFC 6265 section 5.3 ignores the cookie entirely if its Domain
+            # attribute does not match the request host (step 6) before an
+            # existing cookie is removed (steps 11-12)
+            if self._policy.set_ok(cookie, request):
+                try:
+                    self.clear(domain, path, name)
+                except KeyError:
+                    pass
+                _debug("Expiring cookie, domain='%s', path='%s', name='%s'",
+                       domain, path, name)
             return None
 
-        return Cookie(version,
-                      name, value,
-                      port, port_specified,
-                      domain, domain_specified, domain_initial_dot,
-                      path, path_specified,
-                      secure,
-                      expires,
-                      discard,
-                      comment,
-                      comment_url,
-                      rest)
+        return cookie
 
     def _cookies_from_attrs_set(self, attrs_set, request):
         cookie_tuples = self._normalized_cookie_tuples(attrs_set)
diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py
index f4b53bb1932f53b..a7cbc23f23f3427 100644
--- a/Lib/test/test_http_cookiejar.py
+++ b/Lib/test/test_http_cookiejar.py
@@ -743,6 +743,55 @@ def test_expires(self):
 
         # XXX RFC 2965 expiry rules (some apply to V0 too)
 
+    def test_expired_cookie_from_other_domain_is_ignored(self):
+        c = CookieJar()
+        interact_netscape(c, "http://www.victim.test/";,
+                          'session=secret; Domain=.victim.test; Path=/')
+
+        for domain in ('.victim.test', 'victim.test'):
+            for expires in (
+                'Max-Age=0',
+                'Expires=Thu, 01 Jan 1970 00:00:00 GMT',
+            ):
+                hdr = f'session=x; Domain={domain}; Path=/; {expires}'
+                with self.subTest(hdr=hdr):
+                    interact_netscape(c, "http://www.attacker.test/";, hdr)
+                    self.assertEqual(len(c), 1)
+                    self.assertIn(
+                        'session=secret',
+                        interact_netscape(c, "http://www.victim.test/";))
+
+        interact_netscape(c, "http://www.victim.test/";,
+                          'session=x; Domain=.victim.test; Path=/; Max-Age=0')
+        self.assertEqual(len(c), 0)
+
+    def test_expired_cookie_from_other_path_is_ignored(self):
+        pol = DefaultCookiePolicy(strict_ns_set_path=True)
+        c = CookieJar(pol)
+        interact_netscape(c, "http://www.acme.com/a/";,
+                          'token=secret; Path=/a')
+        self.assertEqual(len(c), 1)
+        interact_netscape(c, "http://www.acme.com/b/";,
+                          'token=x; Path=/a; Max-Age=0')
+        self.assertEqual(len(c), 1)
+        interact_netscape(c, "http://www.acme.com/a/";,
+                          'token=x; Path=/a; Max-Age=0')
+        self.assertEqual(len(c), 0)
+
+    def test_expired_cookie_for_blocked_domain_is_ignored(self):
+        c = CookieJar()
+        interact_netscape(c, "http://www.victim.test/";,
+                          'session=secret; Domain=.victim.test; Path=/')
+        self.assertEqual(len(c), 1)
+        c.set_policy(DefaultCookiePolicy(blocked_domains=[".victim.test"]))
+        interact_netscape(c, "http://www.victim.test/";,
+                          'session=x; Domain=.victim.test; Path=/; Max-Age=0')
+        self.assertEqual(len(c), 1)
+        c.set_policy(DefaultCookiePolicy())
+        interact_netscape(c, "http://www.victim.test/";,
+                          'session=x; Domain=.victim.test; Path=/; Max-Age=0')
+        self.assertEqual(len(c), 0)
+
     def test_default_path(self):
         # RFC 2965
         pol = DefaultCookiePolicy(rfc2965=True)
diff --git 
a/Misc/NEWS.d/next/Security/2026-11-07-01-43-49.gh-issue-161569.cookiej.rst 
b/Misc/NEWS.d/next/Security/2026-11-07-01-43-49.gh-issue-161569.cookiej.rst
new file mode 100644
index 000000000000000..b16f6caf90f1b06
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-11-07-01-43-49.gh-issue-161569.cookiej.rst
@@ -0,0 +1,7 @@
+.. cve: CVE-2026-124249
+
+Fix :class:`http.cookiejar.CookieJar` deleting a stored cookie when a
+response from an unrelated host sets an already expired cookie with a
+``Domain`` or ``Path`` attribute pointing at that cookie. The cookie policy
+is now consulted before an expired cookie is allowed to delete an existing
+one.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to