From a67e6c856353c04782f38bca6d6c1c3d3287c653 Mon Sep 17 00:00:00 2001
From: Seth Larson <seth@python.org>
Date: Wed, 22 Apr 2026 14:22:31 -0500
Subject: [PATCH] gh-90309: Base64-encode cookie values embedded in JS (cherry
 picked from commit 76b3923d688c0efc580658476c5f525ec8735104)

Co-authored-by: Seth Larson <seth@python.org>
---
 Lib/http/cookies.py                                                     |    8 ++
 Lib/test/test_http_cookies.py                                           |   28 ++++++----
 Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst |    3 +
 3 files changed, 27 insertions(+), 12 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst

Index: Python-3.4.10/Lib/http/cookies.py
===================================================================
--- Python-3.4.10.orig/Lib/http/cookies.py	2026-04-27 19:20:18.448849762 +0200
+++ Python-3.4.10/Lib/http/cookies.py	2026-04-27 19:20:18.564185731 +0200
@@ -402,17 +402,21 @@
                                 self.key, repr(self.value))
 
     def js_output(self, attrs=None):
+        import base64
         # Print javascript
         output_string = self.OutputString(attrs)
         if _has_control_character(output_string):
             raise CookieError("Control characters are not allowed in cookies")
+        # Base64-encode value to avoid template
+        # injection in cookie values.
+        output_encoded = base64.b64encode(output_string.encode('utf-8')).decode("ascii")
         return """
         <script type="text/javascript">
         <!-- begin hiding
-        document.cookie = \"%s\";
+        document.cookie = atob(\"%s\");
         // end hiding -->
         </script>
-        """ % (output_string.replace('"', r'\"'))
+        """ % (output_encoded,)
 
     def OutputString(self, attrs=None):
         # Build up our result
Index: Python-3.4.10/Lib/test/test_http_cookies.py
===================================================================
--- Python-3.4.10.orig/Lib/test/test_http_cookies.py	2026-04-27 19:20:18.450137228 +0200
+++ Python-3.4.10/Lib/test/test_http_cookies.py	2026-04-27 19:21:36.353760993 +0200
@@ -7,6 +7,7 @@
     requires_resource,
     control_characters_c0,
 )
+import base64
 import unittest
 from http import cookies
 import pickle
@@ -119,20 +120,22 @@
 
         self.assertEqual(C.output(['path']),
             'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
+        cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; Path=/acme; Version=1').decode('ascii')
         self.assertEqual(C.js_output(), r"""
         <script type="text/javascript">
         <!-- begin hiding
-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
+        document.cookie = atob("{}");
         // end hiding -->
         </script>
-        """)
+        """.format(cookie_encoded))
+        cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; Path=/acme').decode('ascii')
         self.assertEqual(C.js_output(['path']), r"""
         <script type="text/javascript">
         <!-- begin hiding
-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
+        document.cookie = atob("{}");
         // end hiding -->
         </script>
-        """)
+        """.format(cookie_encoded))
 
     def test_extended_encode(self):
         # Issue 9824: some browsers don't follow the standard; we now
@@ -220,20 +223,22 @@
 
         self.assertEqual(C.output(['path']),
                          'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
+        expected_encoded_cookie = base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1').decode('ascii')
         self.assertEqual(C.js_output(), r"""
         <script type="text/javascript">
         <!-- begin hiding
-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
+        document.cookie = atob("{}");
         // end hiding -->
         </script>
-        """)
+        """.format(expected_encoded_cookie))
+        expected_encoded_cookie = base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme').decode('ascii')
         self.assertEqual(C.js_output(['path']), r"""
         <script type="text/javascript">
         <!-- begin hiding
-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
+        document.cookie = atob("{}");
         // end hiding -->
         </script>
-        """)
+        """.format(expected_encoded_cookie))
 
     def test_invalid_cookies(self):
         # Accepting these could be a security issue
@@ -290,13 +295,16 @@
             self.assertEqual(
                 M.output(),
                 "Set-Cookie: %s=%s; Path=/foo" % (i, "%s_coded_val" % i))
+            expected_encoded_cookie = base64.b64encode(
+                ("%s=%s; Path=/foo" % (i, "%s_coded_val" % i)).encode("ascii")
+            ).decode('ascii')
             expected_js_output = """
         <script type="text/javascript">
         <!-- begin hiding
-        document.cookie = "%s=%s; Path=/foo";
+        document.cookie = atob("%s");
         // end hiding -->
         </script>
-        """ % (i, "%s_coded_val" % i)
+        """ % (expected_encoded_cookie,)
             self.assertEqual(M.js_output(), expected_js_output)
         for i in ["foo bar", "foo@bar"]:
             # Try some illegal characters
Index: Python-3.4.10/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ Python-3.4.10/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst	2026-04-27 19:20:18.564698369 +0200
@@ -0,0 +1,3 @@
+Base64-encode values when embedding cookies to JavaScript using the
+:meth:`http.cookies.BaseCookie.js_output` method to avoid injection
+and escaping.
