diff --git a/Doc/library/http.cookiejar.rst b/Doc/library/http.cookiejar.rst index 23ddecf873876d..36685e32a507f6 100644 --- a/Doc/library/http.cookiejar.rst +++ b/Doc/library/http.cookiejar.rst @@ -716,9 +716,10 @@ Cookies may have additional non-standard cookie-attributes. These may be accessed using the following methods: -.. method:: Cookie.has_nonstandard_attr(name) +.. method:: Cookie.has_nonstandard_attr(name, case_insensitive=False) - Return ``True`` if cookie has the named cookie-attribute. + Return ``True`` if cookie has the named cookie-attribute. If *case_insensitive* + is true, the name is compared without regard to case. .. method:: Cookie.get_nonstandard_attr(name, default=None) diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index fb0fd2e97999af..e3062ade201b09 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -50,7 +50,7 @@ def _debug(*args): logger = logging.getLogger("http.cookiejar") return logger.debug(*args) -HTTPONLY_ATTR = "HTTPOnly" +HTTPONLY_ATTR = "HttpOnly" HTTPONLY_PREFIX = "#HttpOnly_" DEFAULT_HTTP_PORT = str(http.client.HTTP_PORT) NETSCAPE_MAGIC_RGX = re.compile("#( Netscape)? HTTP Cookie File") @@ -800,7 +800,10 @@ def __init__(self, version, name, value, self._rest = copy.copy(rest) - def has_nonstandard_attr(self, name): + def has_nonstandard_attr(self, name, case_insensitive=False): + if case_insensitive: + name = name.lower() + return any(k.lower() == name for k in self._rest) return name in self._rest def get_nonstandard_attr(self, name, default=None): return self._rest.get(name, default) @@ -2113,7 +2116,7 @@ def save(self, filename=None, ignore_discard=False, ignore_expires=False): else: name = cookie.name value = cookie.value - if cookie.has_nonstandard_attr(HTTPONLY_ATTR): + if cookie.has_nonstandard_attr(HTTPONLY_ATTR, case_insensitive=True): domain = HTTPONLY_PREFIX + domain f.write( "\t".join([domain, initial_dot, cookie.path, diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 25a671809d4499..1e05481cfc9f48 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -623,6 +623,7 @@ def test_ns_parser(self): # case is preserved self.assertTrue(cookie.has_nonstandard_attr("blArgh")) self.assertFalse(cookie.has_nonstandard_attr("blargh")) + self.assertTrue(cookie.has_nonstandard_attr("blargh", case_insensitive=True)) cookie = c._cookies["www.acme.com"]["/"]["ni"] self.assertEqual(cookie.domain, "www.acme.com") @@ -1881,7 +1882,7 @@ def test_mozilla(self): for cookie in c: if cookie.name == "foo1": - cookie.set_nonstandard_attr("HTTPOnly", "") + cookie.set_nonstandard_attr("HttpOnly", "") def save_and_restore(cj, ignore_discard): try: @@ -1896,12 +1897,79 @@ def save_and_restore(cj, ignore_discard): new_c = save_and_restore(c, True) self.assertEqual(len(new_c), 6) # none discarded self.assertIn("name='foo1', value='bar'", repr(new_c)) - self.assertIn("rest={'HTTPOnly': ''}", repr(new_c)) + self.assertIn("rest={'HttpOnly': ''}", repr(new_c)) new_c = save_and_restore(c, False) self.assertEqual(len(new_c), 4) # 2 of them discarded on save self.assertIn("name='foo1', value='bar'", repr(new_c)) + def test_mozilla_httponly_prefix(self): + # Save / load Mozilla/Netscape cookie file with HttpOnly prefix. + filename = os_helper.TESTFN + + # Load the input file test + c1 = MozillaCookieJar(filename) + one_year_later = int(time.time()) + 365*24*60*60 + try: + with open(filename, "w") as f: + f.write("# Netscape HTTP Cookie File\n") + f.write("#HttpOnly_.example.com\tTRUE\t/\tFALSE\t%d\tfoo\tbar\n" + % (one_year_later,)) + c1.load() + finally: + os_helper.unlink(filename) + + cookie = list(c1)[0] + self.assertIn("HttpOnly", repr(cookie)) + self.assertTrue(cookie.has_nonstandard_attr("HttpOnly", case_insensitive=True)) + self.assertTrue(cookie.has_nonstandard_attr("HTTPOnly", case_insensitive=True)) + self.assertFalse(cookie.has_nonstandard_attr("HTTPOnly")) + + # Save and read the output file test + c2 = MozillaCookieJar(filename) + year_plus_one = time.localtime()[0] + 1 + expires = "expires=09-Nov-%d 23:12:40 GMT" % (year_plus_one,) + # foo1 has the HttpOnly flag set + interact_netscape(c2, "http://example.com/", + "foo1=bar1; %s; HttpOnly;" % expires) + # foo2 will have the HttpOnly flag set later + interact_netscape(c2, "http://example.com/", + "foo2=bar2; %s;" % expires) + # foo3 will have the HTTPOnly flag set later + interact_netscape(c2, "http://example.com/", + "foo3=bar3; %s;" % expires) + # foo4 does not have the HttpOnly flag set + interact_netscape(c2, "http://example.com/", + "foo4=bar4; %s;" % expires) + # Set flags manually + for cookie in c2: + if cookie.name == "foo2": + cookie.set_nonstandard_attr("HttpOnly", "") + if cookie.name == "foo3": + cookie.set_nonstandard_attr("HTTPOnly", "") + + # Save and read the output file + try: + c2.save() + with open(filename, "r") as f: + lines = f.readlines() + finally: + os_helper.unlink(filename) + + # Check that the HttpOnly prefix is added to the correct cookies + for key in ["foo1", "foo2", "foo3"]: + with self.subTest(key=key): + matches = [x for x in lines if key in x] + self.assertEqual(len(matches), 1) + self.assertTrue(matches[0].startswith("#HttpOnly_")) + + # Check that the HttpOnly prefix is not added to the correct cookies + for key in ["foo4"]: + with self.subTest(key=key): + matches = [x for x in lines if key in x] + self.assertEqual(len(matches), 1) + self.assertFalse(matches[0].startswith("#HttpOnly_")) + def test_netscape_misc(self): # Some additional Netscape cookies tests. c = CookieJar() diff --git a/Misc/NEWS.d/next/Library/2025-03-23-08-26-53.gh-issue-113775.7-2Dqp.rst b/Misc/NEWS.d/next/Library/2025-03-23-08-26-53.gh-issue-113775.7-2Dqp.rst new file mode 100644 index 00000000000000..f009adcf977e03 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-23-08-26-53.gh-issue-113775.7-2Dqp.rst @@ -0,0 +1 @@ +Fix handling of the ``#HttpOnly_`` prefix in :class:`http.cookiejar.MozillaCookieJar`.