From ad24492057d87f71d9228f739c9ec42697eb7a38 Mon Sep 17 00:00:00 2001 From: kairi003 Date: Sun, 7 Jan 2024 22:54:15 +0900 Subject: [PATCH 1/6] Add case_insensitive option to Cookie.has_nonstandard_attr method Cookie.has_nonstandard_attr method is case-sensitive, but HTTP is case-insensitive for cookie attribute names. This option is useful for MozillaCookieJar to check the HttpOnly flag when saving. --- Doc/library/http.cookiejar.rst | 5 +++-- Lib/http/cookiejar.py | 5 ++++- Lib/test/test_http_cookiejar.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) 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..3cd12e74e60e3c 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -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) diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 25a671809d4499..d4c8442a8fccfd 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") From eb730a34e9dccd3124554074596b1a5367ce84c6 Mon Sep 17 00:00:00 2001 From: kairi003 Date: Sun, 7 Jan 2024 23:21:13 +0900 Subject: [PATCH 2/6] Change HTTPONLY_ATTR value from "HTTPOnly" to "HttpOnly" This commit updates the value of the HTTPONLY_ATTR constant from "HTTPOnly" to the more commonly used "HttpOnly". It makes HTTP communication more consistent. --- Lib/http/cookiejar.py | 2 +- Lib/test/test_http_cookiejar.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index 3cd12e74e60e3c..eac27ecd8391b5 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") diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index d4c8442a8fccfd..462a6b0bb37b16 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -1882,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: @@ -1897,7 +1897,7 @@ 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 From 028b6077bf66cf82305733613f5f6ac67f4989e0 Mon Sep 17 00:00:00 2001 From: kairi003 Date: Mon, 8 Jan 2024 01:50:12 +0900 Subject: [PATCH 3/6] Fix HttpOnly Prefix Issue in MozillaCookieJar.save Modified attribute checking in MozillaCookieJar.save to be case-insensitive, aligning with HTTP standards. This change resolves the issue where HttpOnly prefix was not correctly appended due to case-sensitive checks. --- Lib/http/cookiejar.py | 2 +- Lib/test/test_http_cookiejar.py | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index eac27ecd8391b5..e3062ade201b09 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -2116,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 462a6b0bb37b16..7692d9bc27c4d9 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -1903,6 +1903,75 @@ def save_and_restore(cj, ignore_discard): 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 value in ["foo1", "foo2", "foo3"]: + matches = [x for x in lines if value in x] + self.assertEqual(len(matches), 1, + "Incorrect number of matches for cookie with value %r" % value) + self.assertTrue(matches[0].startswith("#HttpOnly_"), + "Cookie with value %r is missing the HttpOnly prefix" % value) + + # Check that the HttpOnly prefix is not added to the correct cookies + for value in ["foo4"]: + matches = [x for x in lines if value in x] + self.assertEqual(len(matches), 1, + "Incorrect number of matches for cookie with value %r" % value) + self.assertFalse(matches[0].startswith("#HttpOnly_"), + "Cookie with value %r has the HttpOnly prefix" % value) + def test_netscape_misc(self): # Some additional Netscape cookies tests. c = CookieJar() From 63294952bd7de44898b3d4f0529d92a2dc6aa504 Mon Sep 17 00:00:00 2001 From: kairi003 Date: Mon, 8 Jan 2024 03:46:05 +0900 Subject: [PATCH 4/6] Fix loop variable name in test --- Lib/test/test_http_cookiejar.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 7692d9bc27c4d9..97600a142034ac 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -1957,20 +1957,20 @@ def test_mozilla_httponly_prefix(self): os_helper.unlink(filename) # Check that the HttpOnly prefix is added to the correct cookies - for value in ["foo1", "foo2", "foo3"]: - matches = [x for x in lines if value in x] + for key in ["foo1", "foo2", "foo3"]: + matches = [x for x in lines if key in x] self.assertEqual(len(matches), 1, - "Incorrect number of matches for cookie with value %r" % value) + "Incorrect number of matches for cookie with value %r" % key) self.assertTrue(matches[0].startswith("#HttpOnly_"), - "Cookie with value %r is missing the HttpOnly prefix" % value) + "Cookie with value %r is missing the HttpOnly prefix" % key) # Check that the HttpOnly prefix is not added to the correct cookies - for value in ["foo4"]: - matches = [x for x in lines if value in x] + for key in ["foo4"]: + matches = [x for x in lines if key in x] self.assertEqual(len(matches), 1, - "Incorrect number of matches for cookie with value %r" % value) + "Incorrect number of matches for cookie with value %r" % key) self.assertFalse(matches[0].startswith("#HttpOnly_"), - "Cookie with value %r has the HttpOnly prefix" % value) + "Cookie with value %r has the HttpOnly prefix" % key) def test_netscape_misc(self): # Some additional Netscape cookies tests. From 2d1ea7ad825e9e30e2c7534cbfbb53d57e6521b0 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 08:26:54 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-03-23-08-26-53.gh-issue-113775.7-2Dqp.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-03-23-08-26-53.gh-issue-113775.7-2Dqp.rst 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`. From 7f72d4e4aa1e7c83716e8225f6b81d7728dd6f42 Mon Sep 17 00:00:00 2001 From: kairi003 Date: Sun, 23 Mar 2025 22:03:06 +0900 Subject: [PATCH 6/6] Refactor test_mozilla_httponly_prefix to use subTest --- Lib/test/test_http_cookiejar.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 97600a142034ac..1e05481cfc9f48 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -1958,19 +1958,17 @@ def test_mozilla_httponly_prefix(self): # Check that the HttpOnly prefix is added to the correct cookies for key in ["foo1", "foo2", "foo3"]: - matches = [x for x in lines if key in x] - self.assertEqual(len(matches), 1, - "Incorrect number of matches for cookie with value %r" % key) - self.assertTrue(matches[0].startswith("#HttpOnly_"), - "Cookie with value %r is missing the HttpOnly prefix" % key) + 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"]: - matches = [x for x in lines if key in x] - self.assertEqual(len(matches), 1, - "Incorrect number of matches for cookie with value %r" % key) - self.assertFalse(matches[0].startswith("#HttpOnly_"), - "Cookie with value %r has the HttpOnly prefix" % key) + 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.