Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:python3.13
User: [email protected]
Usertags: pu

This is a followup update to the +deb13u1 update already
accepeted to trixie-proposed-updates a few weeks ago,
which fixes a more low severity security issues, which
have since then been backported to the upstream 3.13
branch.

Tests were fine and everything looks in order in the tests
triggered in debusine as well. The debdiff below is relative
to the version already on trixie-p-u.

Cheers,
        Moritz

diff -Nru python3.13-3.13.5/debian/changelog python3.13-3.13.5/debian/changelog
--- python3.13-3.13.5/debian/changelog  2026-04-06 14:24:14.000000000 +0200
+++ python3.13-3.13.5/debian/changelog  2026-05-05 23:05:52.000000000 +0200
@@ -1,3 +1,14 @@
+python3.13 (3.13.5-2+deb13u2) trixie; urgency=medium
+
+  * CVE-2026-3446
+  * CVE-2026-4224
+  * CVE-2026-3644
+  * CVE-2026-4519
+  * CVE-2026-6019 (Closes: #1135116)
+  * CVE-2026-6100
+
+ -- Moritz Mühlenhoff <[email protected]>  Tue, 05 May 2026 23:05:52 +0200
+
 python3.13 (3.13.5-2+deb13u1) trixie; urgency=medium
 
   * CVE-2025-11468 (Closes: #1126787)
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-3446.patch 
python3.13-3.13.5/debian/patches/CVE-2026-3446.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-3446.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-3446.patch        2026-05-05 
14:21:14.000000000 +0200
@@ -0,0 +1,209 @@
+From 1f9958f909c1b41a4ffc0b613ef8ec8fa5e7c474 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Tue, 24 Mar 2026 00:52:20 +0100
+Subject: [PATCH] [3.13] gh-145264: Do not ignore excess Base64 data after the
+ first padded quad (GH-145267) (GH-146326) (GH-146348)
+
+--- python3.13-3.13.5.orig/Lib/test/test_binascii.py
++++ python3.13-3.13.5/Lib/test/test_binascii.py
+@@ -143,17 +143,16 @@ class BinASCIITest(unittest.TestCase):
+             _assertRegexTemplate(r'(?i)Excess padding', data, 
non_strict_mode_expected_result)
+ 
+         # Test excess data exceptions
+-        assertExcessData(b'ab==a', b'i')
+-        assertExcessData(b'ab===', b'i')
+-        assertExcessData(b'ab====', b'i')
+-        assertExcessData(b'ab==:', b'i')
+-        assertExcessData(b'abc=a', b'i\xb7')
+-        assertExcessData(b'abc=:', b'i\xb7')
+-        assertExcessData(b'ab==\n', b'i')
+-        assertExcessData(b'abc==', b'i\xb7')
+-        assertExcessData(b'abc===', b'i\xb7')
+-        assertExcessData(b'abc====', b'i\xb7')
+-        assertExcessData(b'abc=====', b'i\xb7')
++        assertExcessPadding(b'ab===', b'i')
++        assertExcessPadding(b'ab====', b'i')
++        assertNonBase64Data(b'ab==:', b'i')
++        assertExcessData(b'abc=a', b'i\xb7\x1a')
++        assertNonBase64Data(b'abc=:', b'i\xb7')
++        assertNonBase64Data(b'ab==\n', b'i')
++        assertExcessPadding(b'abc==', b'i\xb7')
++        assertExcessPadding(b'abc===', b'i\xb7')
++        assertExcessPadding(b'abc====', b'i\xb7')
++        assertExcessPadding(b'abc=====', b'i\xb7')
+ 
+         # Test non-base64 data exceptions
+         assertNonBase64Data(b'\nab==', b'i')
+@@ -175,6 +174,20 @@ class BinASCIITest(unittest.TestCase):
+         assertExcessPadding(b'abcd====', b'i\xb7\x1d')
+         assertExcessPadding(b'abcd=====', b'i\xb7\x1d')
+ 
++    def test_base64_excess_data(self):
++        # Test excess data exceptions
++        def assertExcessData(data, expected):
++            assert_regex = r'(?i)Excess data'
++            data = self.type2test(data)
++            with self.assertRaisesRegex(binascii.Error, assert_regex):
++                binascii.a2b_base64(data, strict_mode=True)
++            self.assertEqual(binascii.a2b_base64(data, strict_mode=False),
++                             expected)
++            self.assertEqual(binascii.a2b_base64(data), expected)
++
++        assertExcessData(b'ab==c=', b'i\xb7')
++        assertExcessData(b'ab==cd', b'i\xb7\x1d')
++        assertExcessData(b'abc=d', b'i\xb7\x1d')
+ 
+     def test_base64errors(self):
+         # Test base64 with invalid padding
+--- python3.13-3.13.5.orig/Modules/binascii.c
++++ python3.13-3.13.5/Modules/binascii.c
+@@ -383,7 +383,6 @@ binascii_a2b_base64_impl(PyObject *modul
+     const unsigned char *ascii_data = data->buf;
+     size_t ascii_len = data->len;
+     binascii_state *state = NULL;
+-    char padding_started = 0;
+ 
+     /* Allocate the buffer */
+     Py_ssize_t bin_len = ((ascii_len+3)/4)*3; /* Upper bound, corrected later 
*/
+@@ -394,14 +393,6 @@ binascii_a2b_base64_impl(PyObject *modul
+         return NULL;
+     unsigned char *bin_data_start = bin_data;
+ 
+-    if (strict_mode && ascii_len > 0 && ascii_data[0] == '=') {
+-        state = get_binascii_state(module);
+-        if (state) {
+-            PyErr_SetString(state->Error, "Leading padding not allowed");
+-        }
+-        goto error_end;
+-    }
+-
+     int quad_pos = 0;
+     unsigned char leftchar = 0;
+     int pads = 0;
+@@ -412,35 +403,34 @@ binascii_a2b_base64_impl(PyObject *modul
+         ** the invalid ones.
+         */
+         if (this_ch == BASE64_PAD) {
+-            padding_started = 1;
+-
+-            if (strict_mode && quad_pos == 0) {
+-                state = get_binascii_state(module);
+-                if (state) {
+-                    PyErr_SetString(state->Error, "Excess padding not 
allowed");
+-                }
+-                goto error_end;
++            pads++;
++            if (quad_pos >= 2 && quad_pos + pads <= 4) {
++                continue;
+             }
+-            if (quad_pos >= 2 && quad_pos + ++pads >= 4) {
+-                /* A pad sequence means we should not parse more input.
+-                ** We've already interpreted the data from the quad at this 
point.
+-                ** in strict mode, an error should raise if there's excess 
data after the padding.
+-                */
+-                if (strict_mode && i + 1 < ascii_len) {
+-                    state = get_binascii_state(module);
+-                    if (state) {
+-                        PyErr_SetString(state->Error, "Excess data after 
padding");
+-                    }
+-                    goto error_end;
+-                }
+-
+-                goto done;
++            // See RFC 4648, section-3.3: "specifications MAY ignore the
++            // pad character, "=", treating it as non-alphabet data, if
++            // it is present before the end of the encoded data" and
++            // "the excess pad characters MAY also be ignored."
++            if (!strict_mode) {
++                continue;
+             }
+-            continue;
++            if (quad_pos == 1) {
++                /* Set an error below. */
++                break;
++            }
++            state = get_binascii_state(module);
++            if (state) {
++                PyErr_SetString(state->Error,
++                                (quad_pos == 0 && i == 0)
++                                ? "Leading padding not allowed"
++                                : "Excess padding not allowed");
++            }
++            goto error_end;
+         }
+ 
+         this_ch = table_a2b_base64[this_ch];
+         if (this_ch >= 64) {
++            // See RFC 4648, section-3.3.
+             if (strict_mode) {
+                 state = get_binascii_state(module);
+                 if (state) {
+@@ -451,11 +441,14 @@ binascii_a2b_base64_impl(PyObject *modul
+             continue;
+         }
+ 
+-        // Characters that are not '=', in the middle of the padding, are not 
allowed
+-        if (strict_mode && padding_started) {
++        // Characters that are not '=', in the middle of the padding, are
++        // not allowed (except when they are). See RFC 4648, section-3.3.
++        if (pads && strict_mode) {
+             state = get_binascii_state(module);
+             if (state) {
+-                PyErr_SetString(state->Error, "Discontinuous padding not 
allowed");
++                PyErr_SetString(state->Error, (quad_pos + pads == 4)
++                    ? "Excess data after padding"
++                    : "Discontinuous padding not allowed");
+             }
+             goto error_end;
+         }
+@@ -484,31 +477,35 @@ binascii_a2b_base64_impl(PyObject *modul
+         }
+     }
+ 
+-    if (quad_pos != 0) {
++    if (quad_pos == 1) {
++        /* There is exactly one extra valid, non-padding, base64 character.
++         * * This is an invalid length, as there is no possible input that
++         ** could encoded into such a base64 string.
++         */
+         state = get_binascii_state(module);
+-        if (state == NULL) {
+-            /* error already set, from get_binascii_state */
+-        } else if (quad_pos == 1) {
+-            /*
+-            ** There is exactly one extra valid, non-padding, base64 
character.
+-            ** This is an invalid length, as there is no possible input that
+-            ** could encoded into such a base64 string.
+-            */
++        if (state) {
+             PyErr_Format(state->Error,
+                          "Invalid base64-encoded string: "
+                          "number of data characters (%zd) cannot be 1 more "
+                          "than a multiple of 4",
+                          (bin_data - bin_data_start) / 3 * 4 + 1);
+-        } else {
++        }
++        goto error_end;
++    }
++
++    if (quad_pos != 0 && quad_pos + pads < 4) {
++        state = get_binascii_state(module);
++        if (state) {
+             PyErr_SetString(state->Error, "Incorrect padding");
+         }
+-        error_end:
+-        _PyBytesWriter_Dealloc(&writer);
+-        return NULL;
++        goto error_end;
+     }
+ 
+-done:
+     return _PyBytesWriter_Finish(&writer, bin_data);
++
++error_end:
++    _PyBytesWriter_Dealloc(&writer);
++    return NULL;
+ }
+ 
+ 
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-3644.patch 
python3.13-3.13.5/debian/patches/CVE-2026-3644.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-3644.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-3644.patch        2026-05-05 
23:01:49.000000000 +0200
@@ -0,0 +1,124 @@
+From d16ecc6c3626f0e2cc8f08c309c83934e8a979dd Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 16 Mar 2026 15:05:13 +0100
+Subject: [PATCH] [3.13] gh-145599, CVE 2026-3644: Reject control characters in
+ `http.cookies.Morsel.update()` (GH-145600) (#146024)
+
+--- python3.13-3.13.5.orig/Lib/http/cookies.py
++++ python3.13-3.13.5/Lib/http/cookies.py
+@@ -335,9 +335,16 @@ class Morsel(dict):
+             key = key.lower()
+             if key not in self._reserved:
+                 raise CookieError("Invalid attribute %r" % (key,))
++            if _has_control_character(key, val):
++                raise CookieError("Control characters are not allowed in "
++                                  f"cookies {key!r} {val!r}")
+             data[key] = val
+         dict.update(self, data)
+ 
++    def __ior__(self, values):
++        self.update(values)
++        return self
++
+     def isReservedKey(self, K):
+         return K.lower() in self._reserved
+ 
+@@ -363,9 +370,15 @@ class Morsel(dict):
+         }
+ 
+     def __setstate__(self, state):
+-        self._key = state['key']
+-        self._value = state['value']
+-        self._coded_value = state['coded_value']
++        key = state['key']
++        value = state['value']
++        coded_value = state['coded_value']
++        if _has_control_character(key, value, coded_value):
++            raise CookieError("Control characters are not allowed in cookies "
++                              f"{key!r} {value!r} {coded_value!r}")
++        self._key = key
++        self._value = value
++        self._coded_value = coded_value
+ 
+     def output(self, attrs=None, header="Set-Cookie:"):
+         return "%s %s" % (header, self.OutputString(attrs))
+@@ -377,13 +390,16 @@ class Morsel(dict):
+ 
+     def js_output(self, attrs=None):
+         # Print javascript
++        output_string = self.OutputString(attrs)
++        if _has_control_character(output_string):
++            raise CookieError("Control characters are not allowed in cookies")
+         return """
+         <script type="text/javascript">
+         <!-- begin hiding
+         document.cookie = \"%s\";
+         // end hiding -->
+         </script>
+-        """ % (self.OutputString(attrs).replace('"', r'\"'))
++        """ % (output_string.replace('"', r'\"'))
+ 
+     def OutputString(self, attrs=None):
+         # Build up our result
+--- python3.13-3.13.5.orig/Lib/test/test_http_cookies.py
++++ python3.13-3.13.5/Lib/test/test_http_cookies.py
+@@ -574,6 +574,14 @@ class MorselTests(unittest.TestCase):
+             with self.assertRaises(cookies.CookieError):
+                 morsel["path"] = c0
+ 
++            # .__setstate__()
++            with self.assertRaises(cookies.CookieError):
++                morsel.__setstate__({'key': c0, 'value': 'val', 
'coded_value': 'coded'})
++            with self.assertRaises(cookies.CookieError):
++                morsel.__setstate__({'key': 'key', 'value': c0, 
'coded_value': 'coded'})
++            with self.assertRaises(cookies.CookieError):
++                morsel.__setstate__({'key': 'key', 'value': 'val', 
'coded_value': c0})
++
+             # .setdefault()
+             with self.assertRaises(cookies.CookieError):
+                 morsel.setdefault("path", c0)
+@@ -588,6 +596,18 @@ class MorselTests(unittest.TestCase):
+             with self.assertRaises(cookies.CookieError):
+                 morsel.set("path", "val", c0)
+ 
++            # .update()
++            with self.assertRaises(cookies.CookieError):
++                morsel.update({"path": c0})
++            with self.assertRaises(cookies.CookieError):
++                morsel.update({c0: "val"})
++
++            # .__ior__()
++            with self.assertRaises(cookies.CookieError):
++                morsel |= {"path": c0}
++            with self.assertRaises(cookies.CookieError):
++                morsel |= {c0: "val"}
++
+     def test_control_characters_output(self):
+         # Tests that even if the internals of Morsel are modified
+         # that a call to .output() has control character safeguards.
+@@ -608,6 +628,24 @@ class MorselTests(unittest.TestCase):
+             with self.assertRaises(cookies.CookieError):
+                 cookie.output()
+ 
++        # Tests that .js_output() also has control character safeguards.
++        for c0 in support.control_characters_c0():
++            morsel = cookies.Morsel()
++            morsel.set("key", "value", "coded-value")
++            morsel._key = c0  # Override private variable.
++            cookie = cookies.SimpleCookie()
++            cookie["cookie"] = morsel
++            with self.assertRaises(cookies.CookieError):
++                cookie.js_output()
++
++            morsel = cookies.Morsel()
++            morsel.set("key", "value", "coded-value")
++            morsel._coded_value = c0  # Override private variable.
++            cookie = cookies.SimpleCookie()
++            cookie["cookie"] = morsel
++            with self.assertRaises(cookies.CookieError):
++                cookie.js_output()
++
+ 
+ def load_tests(loader, tests, pattern):
+     tests.addTest(doctest.DocTestSuite(cookies))
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-4224.patch 
python3.13-3.13.5/debian/patches/CVE-2026-4224.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-4224.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-4224.patch        2026-05-05 
14:48:22.000000000 +0200
@@ -0,0 +1,71 @@
+From 196edfb06a7458377d4d0f4b3cd41724c1f3bd4a Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 16 Mar 2026 10:09:27 +0100
+Subject: [PATCH] [3.13] gh-145986: Avoid unbound C recursion in
+ `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) (#145996)
+
+--- python3.13-3.13.5.orig/Lib/test/test_pyexpat.py
++++ python3.13-3.13.5/Lib/test/test_pyexpat.py
+@@ -668,6 +668,22 @@ class ChardataBufferTest(unittest.TestCa
+         parser.Parse(xml2, True)
+         self.assertEqual(self.n, 4)
+ 
++    def test_deeply_nested_content_model(self):
++        # This should raise a RecursionError and not crash.
++        # See https://github.com/python/cpython/issues/145986.
++        N = 500_000
++        data = (
++            b'<!DOCTYPE root [\n<!ELEMENT root '
++            + b'(a, ' * N + b'a' + b')' * N
++            + b'>\n]>\n<root/>\n'
++        )
++
++        parser = expat.ParserCreate()
++        parser.ElementDeclHandler = lambda _1, _2: None
++        with support.infinite_recursion():
++            with self.assertRaises(RecursionError):
++                parser.Parse(data)
++
+ class MalformedInputTest(unittest.TestCase):
+     def test1(self):
+         xml = b"\0\r\n"
+--- python3.13-3.13.5.orig/Modules/pyexpat.c
++++ python3.13-3.13.5/Modules/pyexpat.c
+@@ -3,6 +3,7 @@
+ #endif
+ 
+ #include "Python.h"
++#include "pycore_ceval.h"         // _Py_EnterRecursiveCall()
+ #include "pycore_import.h"        // _PyImport_SetModule()
+ #include "pycore_pyhash.h"        // _Py_HashSecret
+ #include "pycore_traceback.h"     // _PyTraceback_Add()
+@@ -522,6 +523,10 @@ static PyObject *
+ conv_content_model(XML_Content * const model,
+                    PyObject *(*conv_string)(const XML_Char *))
+ {
++    if (_Py_EnterRecursiveCall(" in conv_content_model")) {
++        return NULL;
++    }
++
+     PyObject *result = NULL;
+     PyObject *children = PyTuple_New(model->numchildren);
+     int i;
+@@ -533,7 +538,7 @@ conv_content_model(XML_Content * const m
+                                                  conv_string);
+             if (child == NULL) {
+                 Py_XDECREF(children);
+-                return NULL;
++                goto done;
+             }
+             PyTuple_SET_ITEM(children, i, child);
+         }
+@@ -541,6 +546,8 @@ conv_content_model(XML_Content * const m
+                                model->type, model->quant,
+                                conv_string,model->name, children);
+     }
++done:
++    _Py_LeaveRecursiveCall();
+     return result;
+ }
+ 
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-4519.patch 
python3.13-3.13.5/debian/patches/CVE-2026-4519.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-4519.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-4519.patch        2026-05-05 
23:03:56.000000000 +0200
@@ -0,0 +1,206 @@
+From 43fe06b96f6a6cf5cfd5bdab20b8649374956866 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Tue, 24 Mar 2026 00:17:50 +0100
+Subject: [PATCH] [3.13] gh-143930: Reject leading dashes in webbrowser URLs
+ (GH-146215)
+
+From 89bfb8e5ed3c7caa241028f1a4eac5f6275a46a4 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?=C5=81ukasz=20Langa?= <[email protected]>
+Date: Fri, 3 Apr 2026 19:45:02 +0200
+Subject: [PATCH] [3.13] gh-143930: Tweak the exception message and increase
+ test coverage (GH-146476) (GH-148045)
+
+From d6d68494be70bdbda20f89f83801ba52ec37daa4 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Wed, 29 Apr 2026 12:00:10 +0200
+Subject: [PATCH] [3.13] gh-148169: Fix webbrowser `%action` substitution
+ bypass of dash-prefix check (GH-148170) (#148517)
+
+
+--- python3.13-3.13.5.orig/Lib/test/test_webbrowser.py
++++ python3.13-3.13.5/Lib/test/test_webbrowser.py
+@@ -1,3 +1,4 @@
++import io
+ import os
+ import re
+ import shlex
+@@ -55,6 +56,14 @@ class CommandTestMixin:
+             popen_args.pop(popen_args.index(option))
+         self.assertEqual(popen_args, arguments)
+ 
++    def test_reject_dash_prefixes(self):
++        browser = self.browser_class(name=CMD_NAME)
++        with self.assertRaisesRegex(
++            ValueError,
++            r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$"
++        ):
++            browser.open(f"--key=val {URL}")
++
+ 
+ class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase):
+ 
+@@ -109,6 +118,15 @@ class ChromeCommandTest(CommandTestMixin
+                        arguments=[URL],
+                        kw=dict(new=999))
+ 
++    def test_reject_action_dash_prefixes(self):
++        browser = self.browser_class(name=CMD_NAME)
++        with self.assertRaises(ValueError):
++            browser.open('%action--incognito')
++        # new=1: action is "--new-window", so "%action" itself expands to
++        # a dash-prefixed flag even with no dash in the original URL.
++        with self.assertRaises(ValueError):
++            browser.open('%action', new=1)
++
+ 
+ class EdgeCommandTest(CommandTestMixin, unittest.TestCase):
+ 
+@@ -301,6 +319,72 @@ class IOSBrowserTest(unittest.TestCase):
+         self._test('open_new_tab')
+ 
+ 
++class MockPopenPipe:
++    def __init__(self, cmd, mode):
++        self.cmd = cmd
++        self.mode = mode
++        self.pipe = io.StringIO()
++        self._closed = False
++
++    def write(self, buf):
++        self.pipe.write(buf)
++
++    def close(self):
++        self._closed = True
++        return None
++
++
[email protected](sys.platform == "darwin", "macOS specific test")
++@requires_subprocess()
++class MacOSXOSAScriptTest(unittest.TestCase):
++    def setUp(self):
++        # Ensure that 'BROWSER' is not set to 'open' or something else.
++        # See: https://github.com/python/cpython/issues/131254.
++        env = self.enterContext(os_helper.EnvironmentVarGuard())
++        env.unset("BROWSER")
++
++        support.patch(self, os, "popen", self.mock_popen)
++        self.browser = webbrowser.MacOSXOSAScript("default")
++
++    def mock_popen(self, cmd, mode):
++        self.popen_pipe = MockPopenPipe(cmd, mode)
++        return self.popen_pipe
++
++    def test_default(self):
++        browser = webbrowser.get()
++        assert isinstance(browser, webbrowser.MacOSXOSAScript)
++        self.assertEqual(browser.name, "default")
++
++    def test_default_open(self):
++        url = "https://python.org";
++        self.browser.open(url)
++        self.assertTrue(self.popen_pipe._closed)
++        self.assertEqual(self.popen_pipe.cmd, "osascript")
++        script = self.popen_pipe.pipe.getvalue()
++        self.assertEqual(script.strip(), f'open location "{url}"')
++
++    def test_url_quote(self):
++        self.browser.open('https://python.org/"quote";')
++        script = self.popen_pipe.pipe.getvalue()
++        self.assertEqual(
++            script.strip(), 'open location "https://python.org/%22quote%22";'
++        )
++
++    def test_explicit_browser(self):
++        browser = webbrowser.MacOSXOSAScript("safari")
++        browser.open("https://python.org";)
++        script = self.popen_pipe.pipe.getvalue()
++        self.assertIn('tell application "safari"', script)
++        self.assertIn('open location "https://python.org";', script)
++
++    def test_reject_dash_prefixes(self):
++        with self.assertRaisesRegex(
++            ValueError,
++            r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$"
++        ):
++            self.browser.open(f"--key=val {URL}")
++
++
+ class BrowserRegistrationTest(unittest.TestCase):
+ 
+     def setUp(self):
+--- python3.13-3.13.5.orig/Lib/webbrowser.py
++++ python3.13-3.13.5/Lib/webbrowser.py
+@@ -164,6 +164,12 @@ class BaseBrowser:
+     def open_new_tab(self, url):
+         return self.open(url, 2)
+ 
++    @staticmethod
++    def _check_url(url):
++        """Ensures that the URL is safe to pass to subprocesses as a 
parameter"""
++        if url and url.lstrip().startswith("-"):
++            raise ValueError(f"Invalid URL (leading dash disallowed): 
{url!r}")
++
+ 
+ class GenericBrowser(BaseBrowser):
+     """Class for all browsers started with a command
+@@ -181,6 +187,7 @@ class GenericBrowser(BaseBrowser):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         cmdline = [self.name] + [arg.replace("%s", url)
+                                  for arg in self.args]
+         try:
+@@ -201,6 +208,7 @@ class BackgroundBrowser(GenericBrowser):
+         cmdline = [self.name] + [arg.replace("%s", url)
+                                  for arg in self.args]
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         try:
+             if sys.platform[:3] == 'win':
+                 p = subprocess.Popen(cmdline)
+@@ -280,7 +288,9 @@ class UnixBrowser(BaseBrowser):
+             raise Error("Bad 'new' parameter to open(); "
+                         f"expected 0, 1, or 2, got {new}")
+ 
+-        args = [arg.replace("%s", url).replace("%action", action)
++        self._check_url(url.replace("%action", action))
++
++        args = [arg.replace("%action", action).replace("%s", url)
+                 for arg in self.remote_args]
+         args = [arg for arg in args if arg]
+         success = self._invoke(args, True, autoraise, url)
+@@ -358,6 +368,7 @@ class Konqueror(BaseBrowser):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         # XXX Currently I know no way to prevent KFM from opening a new win.
+         if new == 2:
+             action = "newTab"
+@@ -576,6 +587,7 @@ if sys.platform[:3] == "win":
+     class WindowsDefault(BaseBrowser):
+         def open(self, url, new=0, autoraise=True):
+             sys.audit("webbrowser.open", url)
++            self._check_url(url)
+             try:
+                 os.startfile(url)
+             except OSError:
+@@ -596,6 +608,7 @@ if sys.platform == 'darwin':
+ 
+         def open(self, url, new=0, autoraise=True):
+             sys.audit("webbrowser.open", url)
++            self._check_url(url)
+             url = url.replace('"', '%22')
+             if self.name == 'default':
+                 script = f'open location "{url}"'  # opens in default browser
+@@ -627,6 +640,7 @@ if sys.platform == "ios":
+     class IOSBrowser(BaseBrowser):
+         def open(self, url, new=0, autoraise=True):
+             sys.audit("webbrowser.open", url)
++            self._check_url(url)
+             # If ctypes isn't available, we can't open a browser
+             if objc is None:
+                 return False
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-6019.patch 
python3.13-3.13.5/debian/patches/CVE-2026-6019.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-6019.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-6019.patch        2026-05-05 
23:04:58.000000000 +0200
@@ -0,0 +1,109 @@
+From 3c59b8b53fc75c7f9578d16fb8201ceb43e8f76c Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Thu, 23 Apr 2026 15:05:17 +0200
+Subject: [PATCH] [3.13] gh-90309: Base64-encode cookie values embedded in JS
+ (GH-148888)
+
+--- python3.13-3.13.5.orig/Lib/http/cookies.py
++++ python3.13-3.13.5/Lib/http/cookies.py
+@@ -389,17 +389,21 @@ class Morsel(dict):
+         return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
+ 
+     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
+--- python3.13-3.13.5.orig/Lib/test/test_http_cookies.py
++++ python3.13-3.13.5/Lib/test/test_http_cookies.py
+@@ -1,5 +1,5 @@
+ # Simple test suite for http/cookies.py
+-
++import base64
+ import copy
+ import unittest
+ import doctest
+@@ -153,17 +153,19 @@ class CookieTests(unittest.TestCase, Ext
+ 
+         self.assertEqual(C.output(['path']),
+             'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
+-        self.assertEqual(C.js_output(), r"""
++        cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; 
Path=/acme; Version=1').decode('ascii')
++        self.assertEqual(C.js_output(), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
++        document.cookie = atob("{cookie_encoded}");
+         // end hiding -->
+         </script>
+         """)
+-        self.assertEqual(C.js_output(['path']), r"""
++        cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; 
Path=/acme').decode('ascii')
++        self.assertEqual(C.js_output(['path']), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
++        document.cookie = atob("{cookie_encoded}");
+         // end hiding -->
+         </script>
+         """)
+@@ -260,17 +262,19 @@ class CookieTests(unittest.TestCase, Ext
+ 
+         self.assertEqual(C.output(['path']),
+                          'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
+-        self.assertEqual(C.js_output(), r"""
++        expected_encoded_cookie = 
base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme; 
Version=1').decode('ascii')
++        self.assertEqual(C.js_output(), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
++        document.cookie = atob("{expected_encoded_cookie}");
+         // end hiding -->
+         </script>
+         """)
+-        self.assertEqual(C.js_output(['path']), r"""
++        expected_encoded_cookie = 
base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme').decode('ascii')
++        self.assertEqual(C.js_output(['path']), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
++        document.cookie = atob("{expected_encoded_cookie}");
+         // end hiding -->
+         </script>
+         """)
+@@ -361,13 +365,16 @@ class MorselTests(unittest.TestCase):
+             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
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-6100.patch 
python3.13-3.13.5/debian/patches/CVE-2026-6100.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-6100.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-6100.patch        2026-05-05 
23:05:45.000000000 +0200
@@ -0,0 +1,37 @@
+From c3cf71c3366fe49acb776a639405c0eea6169c20 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 13 Apr 2026 03:35:24 +0200
+Subject: [PATCH] [3.13] gh-148395: Fix a possible UAF in
+ `{LZMA,BZ2,_Zlib}Decompressor` (GH-148396) (#148479)
+
+--- python3.13-3.13.5.orig/Modules/_bz2module.c
++++ python3.13-3.13.5/Modules/_bz2module.c
+@@ -589,6 +589,7 @@ decompress(BZ2Decompressor *d, char *dat
+     return result;
+ 
+ error:
++    bzs->next_in = NULL;
+     Py_XDECREF(result);
+     return NULL;
+ }
+--- python3.13-3.13.5.orig/Modules/_lzmamodule.c
++++ python3.13-3.13.5/Modules/_lzmamodule.c
+@@ -1112,6 +1112,7 @@ decompress(Decompressor *d, uint8_t *dat
+     return result;
+ 
+ error:
++    lzs->next_in = NULL;
+     Py_XDECREF(result);
+     return NULL;
+ }
+--- python3.13-3.13.5.orig/Modules/zlibmodule.c
++++ python3.13-3.13.5/Modules/zlibmodule.c
+@@ -1648,6 +1648,7 @@ decompress(ZlibDecompressor *self, uint8
+     return result;
+ 
+ error:
++    self->zst.next_in = NULL;
+     Py_XDECREF(result);
+     return NULL;
+ }
diff -Nru python3.13-3.13.5/debian/patches/series 
python3.13-3.13.5/debian/patches/series
--- python3.13-3.13.5/debian/patches/series     2026-04-06 14:23:46.000000000 
+0200
+++ python3.13-3.13.5/debian/patches/series     2026-05-05 23:05:32.000000000 
+0200
@@ -42,3 +42,9 @@
 CVE-2026-0865.patch
 CVE-2026-1299.patch
 CVE-2026-2297.patch
+CVE-2026-3446.patch
+CVE-2026-4224.patch
+CVE-2026-3644.patch
+CVE-2026-4519.patch
+CVE-2026-6019.patch
+CVE-2026-6100.patch

Reply via email to