Your message dated Sat, 11 Jul 2026 10:32:48 +0000
with message-id <[email protected]>
and subject line Released in 13.6
has caused the Debian Bug report #1139577,
regarding trixie-pu: package python3.13/3.13.5-2+deb13u3
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1139577: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1139577
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
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 update fixes five low impact security issues in Python, all
patches were cherrypicked from the upstream 3.13 branch. All tests
triggered via debusine look good. Debdiff below.

diff -Nru python3.13-3.13.5/debian/changelog python3.13-3.13.5/debian/changelog
--- python3.13-3.13.5/debian/changelog  2026-05-05 23:05:52.000000000 +0200
+++ python3.13-3.13.5/debian/changelog  2026-06-08 22:58:08.000000000 +0200
@@ -1,3 +1,13 @@
+python3.13 (3.13.5-2+deb13u3) trixie; urgency=medium
+
+  * CVE-2026-1502
+  * CVE-2026-3276
+  * CVE-2026-7774
+  * CVE-2026-8328
+  * CVE-2026-9669
+
+ -- Moritz Mühlenhoff <[email protected]>  Mon, 08 Jun 2026 22:58:08 +0200
+
 python3.13 (3.13.5-2+deb13u2) trixie; urgency=medium
 
   * CVE-2026-3446
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-1502.patch 
python3.13-3.13.5/debian/patches/CVE-2026-1502.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-1502.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-1502.patch        2026-06-08 
22:55:25.000000000 +0200
@@ -0,0 +1,87 @@
+From 9e071c9b28c17f347f81b388a003d4eeb3c7a8dd Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 18 May 2026 19:44:36 +0200
+Subject: [PATCH] [3.13] gh-146211: Reject CR/LF in HTTP tunnel request headers
+ (GH-146212) (#148343)
+
+--- python3.13-3.13.5.orig/Lib/http/client.py
++++ python3.13-3.13.5/Lib/http/client.py
+@@ -972,13 +972,22 @@ class HTTPConnection:
+         return ip
+ 
+     def _tunnel(self):
++        if _contains_disallowed_url_pchar_re.search(self._tunnel_host):
++            raise ValueError('Tunnel host can\'t contain control characters 
%r'
++                             % (self._tunnel_host,))
+         connect = b"CONNECT %s:%d %s\r\n" % (
+             self._wrap_ipv6(self._tunnel_host.encode("idna")),
+             self._tunnel_port,
+             self._http_vsn_str.encode("ascii"))
+         headers = [connect]
+         for header, value in self._tunnel_headers.items():
+-            headers.append(f"{header}: {value}\r\n".encode("latin-1"))
++            header_bytes = header.encode("latin-1")
++            value_bytes = value.encode("latin-1")
++            if not _is_legal_header_name(header_bytes):
++                raise ValueError('Invalid header name %r' % (header_bytes,))
++            if _is_illegal_header_value(value_bytes):
++                raise ValueError('Invalid header value %r' % (value_bytes,))
++            headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes))
+         headers.append(b"\r\n")
+         # Making a single send() call instead of one per line encourages
+         # the host OS to use a more optimal packet size instead of
+--- python3.13-3.13.5.orig/Lib/test/test_httplib.py
++++ python3.13-3.13.5/Lib/test/test_httplib.py
+@@ -370,6 +370,51 @@ class HeaderTests(TestCase, ExtraAsserti
+                 with self.assertRaisesRegex(ValueError, 'Invalid header'):
+                     conn.putheader(name, value)
+ 
++    def test_invalid_tunnel_headers(self):
++        cases = (
++            ('Invalid\r\nName', 'ValidValue'),
++            ('Invalid\rName', 'ValidValue'),
++            ('Invalid\nName', 'ValidValue'),
++            ('\r\nInvalidName', 'ValidValue'),
++            ('\rInvalidName', 'ValidValue'),
++            ('\nInvalidName', 'ValidValue'),
++            (' InvalidName', 'ValidValue'),
++            ('\tInvalidName', 'ValidValue'),
++            ('Invalid:Name', 'ValidValue'),
++            (':InvalidName', 'ValidValue'),
++            ('ValidName', 'Invalid\r\nValue'),
++            ('ValidName', 'Invalid\rValue'),
++            ('ValidName', 'Invalid\nValue'),
++            ('ValidName', 'InvalidValue\r\n'),
++            ('ValidName', 'InvalidValue\r'),
++            ('ValidName', 'InvalidValue\n'),
++        )
++        for name, value in cases:
++            with self.subTest((name, value)):
++                conn = client.HTTPConnection('example.com')
++                conn.set_tunnel('tunnel', headers={
++                    name: value
++                })
++                conn.sock = FakeSocket('')
++                with self.assertRaisesRegex(ValueError, 'Invalid header'):
++                    conn._tunnel()  # Called in .connect()
++
++    def test_invalid_tunnel_host(self):
++        cases = (
++            'invalid\r.host',
++            '\ninvalid.host',
++            'invalid.host\r\n',
++            'invalid.host\x00',
++            'invalid host',
++        )
++        for tunnel_host in cases:
++            with self.subTest(tunnel_host):
++                conn = client.HTTPConnection('example.com')
++                conn.set_tunnel(tunnel_host)
++                conn.sock = FakeSocket('')
++                with self.assertRaisesRegex(ValueError, 'Tunnel host can\'t 
contain control characters'):
++                    conn._tunnel()  # Called in .connect()
++
+     def test_headers_debuglevel(self):
+         body = (
+             b'HTTP/1.1 200 OK\r\n'
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-3276.patch 
python3.13-3.13.5/debian/patches/CVE-2026-3276.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-3276.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-3276.patch        2026-06-08 
22:56:22.000000000 +0200
@@ -0,0 +1,231 @@
+From ba785b88add96acbf403d65cb157fb2743a33a32 Mon Sep 17 00:00:00 2001
+From: Petr Viktorin <[email protected]>
+Date: Tue, 2 Jun 2026 18:12:42 +0200
+Subject: [PATCH] [3.13] gh-149079: Fix O(n^2) canonical ordering in
+ unicodedata.normalize() (GH-149080) (#150780)
+
+--- python3.13-3.13.5.orig/Lib/test/test_unicodedata.py
++++ python3.13-3.13.5/Lib/test/test_unicodedata.py
+@@ -229,6 +229,34 @@ class UnicodeFunctionsTest(UnicodeDataba
+         b = 'C\u0338' * 20  + '\xC7'
+         self.assertEqual(self.db.normalize('NFC', a), b)
+ 
++    def test_long_combining_mark_run(self):
++        # gh-149079: avoid quadratic canonical ordering.
++        payload = "a" + ("\u0300\u0327" * 32)
++        nfd = "a" + ("\u0327" * 32) + ("\u0300" * 32)
++        nfc = "\u00e0" + ("\u0327" * 32) + ("\u0300" * 31)
++
++        self.assertEqual(self.db.normalize("NFD", payload), nfd)
++        self.assertEqual(self.db.normalize("NFKD", payload), nfd)
++        self.assertEqual(self.db.normalize("NFC", payload), nfc)
++        self.assertEqual(self.db.normalize("NFKC", payload), nfc)
++
++    def test_combining_mark_run_fast_paths(self):
++        # gh-149079: cover short runs and already-sorted long runs.
++        short_payload = "a" + ("\u0300\u0327" * 9) + "\u0300"
++        short_nfd = "a" + ("\u0327" * 9) + ("\u0300" * 10)
++        short_nfc = "\u00e0" + ("\u0327" * 9) + ("\u0300" * 9)
++        long_sorted = "a" + ("\u0327" * 30) + ("\u0300" * 30)
++        long_sorted_nfc = "\u00e0" + ("\u0327" * 30) + ("\u0300" * 29)
++
++        self.assertEqual(self.db.normalize("NFD", short_payload), short_nfd)
++        self.assertEqual(self.db.normalize("NFKD", short_payload), short_nfd)
++        self.assertEqual(self.db.normalize("NFC", short_payload), short_nfc)
++        self.assertEqual(self.db.normalize("NFKC", short_payload), short_nfc)
++        self.assertEqual(self.db.normalize("NFD", long_sorted), long_sorted)
++        self.assertEqual(self.db.normalize("NFKD", long_sorted), long_sorted)
++        self.assertEqual(self.db.normalize("NFC", long_sorted), 
long_sorted_nfc)
++        self.assertEqual(self.db.normalize("NFKC", long_sorted), 
long_sorted_nfc)
++
+     def test_issue29456(self):
+         # Fix #29456
+         u1176_str_a = '\u1100\u1176\u11a8'
+--- python3.13-3.13.5.orig/Modules/unicodedata.c
++++ python3.13-3.13.5/Modules/unicodedata.c
+@@ -488,19 +488,80 @@ get_decomp_record(PyObject *self, Py_UCS
+ #define NCount  (VCount*TCount)
+ #define SCount  (LCount*NCount)
+ 
++/* Small combining runs are usually cheaper with insertion sort. */
++#define CANONICAL_ORDERING_COUNTING_SORT_THRESHOLD 20
++
++static void
++canonical_ordering_sort_insertion(int kind, void *data,
++                                  Py_ssize_t start, Py_ssize_t end)
++{
++    for (Py_ssize_t i = start + 1; i < end; i++) {
++        Py_UCS4 code = PyUnicode_READ(kind, data, i);
++        unsigned char combining = _getrecord_ex(code)->combining;
++        Py_ssize_t j = i;
++
++        while (j > start) {
++            Py_UCS4 previous = PyUnicode_READ(kind, data, j - 1);
++            if (_getrecord_ex(previous)->combining <= combining) {
++                break;
++            }
++            PyUnicode_WRITE(kind, data, j, previous);
++            j--;
++        }
++        if (j != i) {
++            PyUnicode_WRITE(kind, data, j, code);
++        }
++    }
++}
++
++static void
++canonical_ordering_sort_counting(int kind, void *data,
++                                 Py_ssize_t start, Py_ssize_t end,
++                                 Py_UCS4 *sortbuf)
++{
++    Py_ssize_t counts[256] = {0};
++    Py_ssize_t run_length = end - start;
++    Py_ssize_t total = 0;
++
++    for (Py_ssize_t i = start; i < end; i++) {
++        Py_UCS4 code = PyUnicode_READ(kind, data, i);
++        unsigned char combining = _getrecord_ex(code)->combining;
++        counts[combining]++;
++    }
++
++    for (size_t i = 0; i < Py_ARRAY_LENGTH(counts); i++) {
++        Py_ssize_t count = counts[i];
++        counts[i] = total;
++        total += count;
++    }
++
++    /* Reuse counts[] as the next output slot for each CCC. */
++    for (Py_ssize_t i = start; i < end; i++) {
++        Py_UCS4 code = PyUnicode_READ(kind, data, i);
++        unsigned char combining = _getrecord_ex(code)->combining;
++        sortbuf[counts[combining]++] = code;
++    }
++    for (Py_ssize_t i = 0; i < run_length; i++) {
++        PyUnicode_WRITE(kind, data, start + i, sortbuf[i]);
++    }
++}
++
+ static PyObject*
+ nfd_nfkd(PyObject *self, PyObject *input, int k)
+ {
+     PyObject *result;
+     Py_UCS4 *output;
+     Py_ssize_t i, o, osize;
+-    int kind;
+-    const void *data;
++    int input_kind, result_kind;
++    const void *input_data;
++    void *result_data;
+     /* Longest decomposition in Unicode 3.2: U+FDFA */
+     Py_UCS4 stack[20];
+     Py_ssize_t space, isize;
+     int index, prefix, count, stackptr;
+     unsigned char prev, cur;
++    Py_UCS4 *sortbuf = NULL;
++    Py_ssize_t sortbuflen = 0;
+ 
+     stackptr = 0;
+     isize = PyUnicode_GET_LENGTH(input);
+@@ -520,11 +581,11 @@ nfd_nfkd(PyObject *self, PyObject *input
+         return NULL;
+     }
+     i = o = 0;
+-    kind = PyUnicode_KIND(input);
+-    data = PyUnicode_DATA(input);
++    input_kind = PyUnicode_KIND(input);
++    input_data = PyUnicode_DATA(input);
+ 
+     while (i < isize) {
+-        stack[stackptr++] = PyUnicode_READ(kind, data, i++);
++        stack[stackptr++] = PyUnicode_READ(input_kind, input_data, i++);
+         while(stackptr) {
+             Py_UCS4 code = stack[--stackptr];
+             /* Hangul Decomposition adds three characters in
+@@ -589,35 +650,66 @@ nfd_nfkd(PyObject *self, PyObject *input
+     PyMem_Free(output);
+     if (!result)
+         return NULL;
++
+     /* result is guaranteed to be ready, as it is compact. */
+-    kind = PyUnicode_KIND(result);
+-    data = PyUnicode_DATA(result);
++    result_kind = PyUnicode_KIND(result);
++    result_data = PyUnicode_DATA(result);
+ 
+-    /* Sort canonically. */
++    /* Sort each consecutive combining-character run canonically. */
+     i = 0;
+-    prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
+-    for (i++; i < PyUnicode_GET_LENGTH(result); i++) {
+-        cur = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
+-        if (prev == 0 || cur == 0 || prev <= cur) {
+-            prev = cur;
++    while (i < o) {
++        Py_ssize_t run_length, run_start;
++        int needs_sort = 0;
++
++        Py_UCS4 ch = PyUnicode_READ(result_kind, result_data, i);
++        prev = _getrecord_ex(ch)->combining;
++        if (prev == 0) {
++            i++;
+             continue;
+         }
+-        /* Non-canonical order. Need to switch *i with previous. */
+-        o = i - 1;
+-        while (1) {
+-            Py_UCS4 tmp = PyUnicode_READ(kind, data, o+1);
+-            PyUnicode_WRITE(kind, data, o+1,
+-                            PyUnicode_READ(kind, data, o));
+-            PyUnicode_WRITE(kind, data, o, tmp);
+-            o--;
+-            if (o < 0)
+-                break;
+-            prev = _getrecord_ex(PyUnicode_READ(kind, data, o))->combining;
+-            if (prev == 0 || prev <= cur)
++
++        run_start = i++;
++        while (i < o) {
++            Py_UCS4 ch = PyUnicode_READ(result_kind, result_data, i);
++            cur = _getrecord_ex(ch)->combining;
++            if (cur == 0) {
+                 break;
++            }
++            if (prev > cur) {
++                needs_sort = 1;
++            }
++            prev = cur;
++            i++;
+         }
+-        prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
++        if (!needs_sort) {
++            continue;
++        }
++
++        run_length = i - run_start;
++        if (run_length < CANONICAL_ORDERING_COUNTING_SORT_THRESHOLD) {
++            canonical_ordering_sort_insertion(result_kind, result_data,
++                                              run_start, i);
++            continue;
++        }
++
++        if (run_length > sortbuflen) {
++            Py_UCS4 *new_sortbuf = PyMem_Resize(sortbuf,
++                                                Py_UCS4,
++                                                run_length);
++            if (new_sortbuf == NULL) {
++                PyErr_NoMemory();
++                PyMem_Free(sortbuf);
++                Py_DECREF(result);
++                return NULL;
++            }
++            sortbuf = new_sortbuf;
++            sortbuflen = run_length;
++        }
++
++        canonical_ordering_sort_counting(result_kind, result_data,
++                                         run_start, i, sortbuf);
+     }
++    PyMem_Free(sortbuf);
+     return result;
+ }
+ 
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-7774.patch 
python3.13-3.13.5/debian/patches/CVE-2026-7774.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-7774.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-7774.patch        2026-06-08 
22:57:15.000000000 +0200
@@ -0,0 +1,225 @@
+From 0478bd83d82b255e0f29f613367a59d261e7eaa2 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 11 May 2026 11:58:26 +0200
+Subject: [PATCH] [3.13] gh-149486: tarfile.data_filter: validate written link
+ target (GH-149487) (GH-149555)
+
+--- python3.13-3.13.5.orig/Lib/tarfile.py
++++ python3.13-3.13.5/Lib/tarfile.py
+@@ -819,16 +819,22 @@ def _get_filtered_attrs(member, dest_pat
+         if member.islnk() or member.issym():
+             if os.path.isabs(member.linkname):
+                 raise AbsoluteLinkError(member)
++            # A link member that resolves to the destination directory itself
++            # would replace it with a (sym)link, redirecting the destination
++            # for all subsequent members.
++            if target_path == dest_path:
++                raise OutsideDestinationError(member, target_path)
+             normalized = os.path.normpath(member.linkname)
+             if normalized != member.linkname:
+                 new_attrs['linkname'] = normalized
+             if member.issym():
+-                target_path = os.path.join(dest_path,
+-                                           os.path.dirname(name),
+-                                           member.linkname)
++                # The symlink is created at `name` with trailing separators
++                # stripped, so its target is relative to the directory
++                # containing that path.
++                link_dir = os.path.dirname(name.rstrip('/' + os.sep))
++                target_path = os.path.join(dest_path, link_dir, normalized)
+             else:
+-                target_path = os.path.join(dest_path,
+-                                           member.linkname)
++                target_path = os.path.join(dest_path, normalized)
+             target_path = os.path.realpath(target_path,
+                                            strict=os.path.ALLOW_MISSING)
+             if os.path.commonpath([target_path, dest_path]) != dest_path:
+--- python3.13-3.13.5.orig/Lib/test/test_tarfile.py
++++ python3.13-3.13.5/Lib/test/test_tarfile.py
+@@ -3588,6 +3588,39 @@ class TestExtractionFilters(unittest.Tes
+     # The destination for the extraction, within `outerdir`
+     destdir = outerdir / 'dest'
+ 
++    @classmethod
++    def setUpClass(cls):
++        # Posix and Windows have different pathname resolution:
++        # either symlink or a '..' component resolve first.
++        # Let's see which we are on.
++        if os_helper.can_symlink():
++            testpath = os.path.join(TEMPDIR, 'resolution_test')
++            os.mkdir(testpath)
++
++            # testpath/current links to `.` which is all of:
++            #   - `testpath`
++            #   - `testpath/current`
++            #   - `testpath/current/current`
++            #   - etc.
++            os.symlink('.', os.path.join(testpath, 'current'))
++
++            # we'll test where `testpath/current/../file` ends up
++            with open(os.path.join(testpath, 'current', '..', 'file'), 'w'):
++                pass
++
++            if os.path.exists(os.path.join(testpath, 'file')):
++                # Windows collapses 'current\..' to '.' first, leaving
++                # 'testpath\file'
++                cls.dotdot_resolves_early = True
++            elif os.path.exists(os.path.join(testpath, '..', 'file')):
++                # Posix resolves 'current' to '.' first, leaving
++                # 'testpath/../file'
++                cls.dotdot_resolves_early = False
++            else:
++                raise AssertionError('Could not determine link resolution')
++        else:
++            cls.dotdot_resolves_early = False
++
+     @contextmanager
+     def check_context(self, tar, filter, *, check_flag=True):
+         """Extracts `tar` to `self.destdir` and allows checking the result
+@@ -3759,10 +3792,19 @@ class TestExtractionFilters(unittest.Tes
+                     + "which is outside the destination")
+ 
+             with self.check_context(arc.open(), 'data'):
+-                self.expect_exception(
+-                    tarfile.LinkOutsideDestinationError,
+-                    """'parent' would link to ['"].*outerdir['"], """
+-                    + "which is outside the destination")
++                if self.dotdot_resolves_early:
++                    # 'current/../..' normalises to '..', which is rejected.
++                    self.expect_exception(
++                        tarfile.LinkOutsideDestinationError,
++                        """'parent' would link to ['"].*outerdir['"], """
++                        + "which is outside the destination")
++                else:
++                    # 'current/..' normalises to '.'; the rewritten link is
++                    # created and 'parent/evil' lands harmlessly inside the
++                    # destination.
++                    self.expect_file('current', symlink_to='.')
++                    self.expect_file('parent', symlink_to='.')
++                    self.expect_file('evil')
+ 
+         else:
+             # No symlink support. The symlinks are ignored.
+@@ -3852,35 +3894,6 @@ class TestExtractionFilters(unittest.Tes
+         # Test interplaying symlinks
+         # Inspired by 'dirsymlink2b' in jwilk/traversal-archives
+ 
+-        # Posix and Windows have different pathname resolution:
+-        # either symlink or a '..' component resolve first.
+-        # Let's see which we are on.
+-        if os_helper.can_symlink():
+-            testpath = os.path.join(TEMPDIR, 'resolution_test')
+-            os.mkdir(testpath)
+-
+-            # testpath/current links to `.` which is all of:
+-            #   - `testpath`
+-            #   - `testpath/current`
+-            #   - `testpath/current/current`
+-            #   - etc.
+-            os.symlink('.', os.path.join(testpath, 'current'))
+-
+-            # we'll test where `testpath/current/../file` ends up
+-            with open(os.path.join(testpath, 'current', '..', 'file'), 'w'):
+-                pass
+-
+-            if os.path.exists(os.path.join(testpath, 'file')):
+-                # Windows collapses 'current\..' to '.' first, leaving
+-                # 'testpath\file'
+-                dotdot_resolves_early = True
+-            elif os.path.exists(os.path.join(testpath, '..', 'file')):
+-                # Posix resolves 'current' to '.' first, leaving
+-                # 'testpath/../file'
+-                dotdot_resolves_early = False
+-            else:
+-                raise AssertionError('Could not determine link resolution')
+-
+         with ArchiveMaker() as arc:
+ 
+             # `current` links to `.` which is both the destination directory
+@@ -3916,7 +3929,7 @@ class TestExtractionFilters(unittest.Tes
+ 
+         with self.check_context(arc.open(), 'data'):
+             if os_helper.can_symlink():
+-                if dotdot_resolves_early:
++                if self.dotdot_resolves_early:
+                     # Fail when extracting a file outside destination
+                     self.expect_exception(
+                             tarfile.OutsideDestinationError,
+@@ -4037,6 +4050,76 @@ class TestExtractionFilters(unittest.Tes
+                     + "destination")
+ 
+     @symlink_test
++    @os_helper.skip_unless_symlink
++    def test_normpath_realpath_mismatch(self):
++        # The link-target check must validate the value that will actually
++        # be written to disk (the normalised linkname), not the original.
++        # Here 'a' is a symlink to a deep nonexistent path, so realpath()
++        # of 'a/../../...' stays inside the destination while normpath()
++        # collapses 'a/..' lexically and escapes.
++        depth = len(self.destdir.parts) + 5
++        deep = '/'.join(f'p{i}' for i in range(depth))
++        sneaky = 'a/' + '../' * depth + 'flag'
++        for kind in 'symlink_to', 'hardlink_to':
++            with self.subTest(kind):
++                with ArchiveMaker() as arc:
++                    arc.add('a', symlink_to=deep)
++                    arc.add('escape', **{kind: sneaky})
++                with self.check_context(arc.open(), 'data'):
++                    self.expect_exception(
++                        tarfile.LinkOutsideDestinationError)
++
++    @symlink_test
++    @os_helper.skip_unless_symlink
++    def test_symlink_trailing_slash(self):
++        # A trailing slash on a symlink member's name must not cause the
++        # link target to be resolved relative to the wrong directory.
++        with ArchiveMaker() as arc:
++            t = tarfile.TarInfo('x/')
++            t.type = tarfile.SYMTYPE
++            t.linkname = '..'
++            arc.tar_w.addfile(t)
++            arc.add('x/escaped', content='hi')
++
++        with self.check_context(arc.open(), 'data'):
++            self.expect_exception(tarfile.LinkOutsideDestinationError)
++
++    @symlink_test
++    @os_helper.skip_unless_symlink
++    def test_link_at_destination(self):
++        # A link member whose name resolves to the destination directory
++        # itself must be rejected: otherwise the destination is replaced
++        # by a symlink and later members can be redirected through it.
++        for name in '', '.', './':
++            with ArchiveMaker() as arc:
++                t = tarfile.TarInfo(name)
++                t.type = tarfile.SYMTYPE
++                t.linkname = '.'
++                arc.tar_w.addfile(t)
++
++            with self.check_context(arc.open(), 'data'):
++                self.expect_exception(tarfile.OutsideDestinationError)
++
++    @symlink_test
++    @os_helper.skip_unless_symlink
++    def test_empty_name_symlink_chain(self):
++        # Regression test for a chain of empty-named symlinks that
++        # incrementally redirects the destination outwards.
++        with ArchiveMaker() as arc:
++            for name, target in [('', ''), ('a/', '..'),
++                                 ('', 'dummy'), ('', 'a'),
++                                 ('b/', '..'),
++                                 ('', 'dummy'), ('', 'a/b')]:
++                t = tarfile.TarInfo(name)
++                t.type = tarfile.SYMTYPE
++                t.linkname = target
++                arc.tar_w.addfile(t)
++            arc.add('escaped', content='hi')
++
++        with self.check_context(arc.open(), 'data'):
++            self.expect_exception(tarfile.FilterError)
++
++    @symlink_test
+     def test_deep_symlink(self):
+         # Test that symlinks and hardlinks inside a directory
+         # point to the correct file (`target` of size 3).
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-8328.patch 
python3.13-3.13.5/debian/patches/CVE-2026-8328.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-8328.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-8328.patch        2026-06-08 
22:58:04.000000000 +0200
@@ -0,0 +1,79 @@
+From bb3446dda6c49b32e67c11dbbbf221b40be00763 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Wed, 13 May 2026 19:58:26 +0200
+Subject: [PATCH] [3.13] gh-87451: Apply CVE-2021-4189 PASV fix to
+ ftplib.ftpcp() (GH-149648) (#149794)
+
+--- python3.13-3.13.5.orig/Lib/ftplib.py
++++ python3.13-3.13.5/Lib/ftplib.py
+@@ -883,7 +883,16 @@ def ftpcp(source, sourcename, target, ta
+     type = 'TYPE ' + type
+     source.voidcmd(type)
+     target.voidcmd(type)
+-    sourcehost, sourceport = parse227(source.sendcmd('PASV'))
++    # Don't trust the IPv4 address the source server advertises in its PASV
++    # reply: a malicious source could otherwise point the target's data
++    # connection at an arbitrary host (SSRF).  A caller that needs the old
++    # behavior can set trust_server_pasv_ipv4_address on the source FTP
++    # object.  See FTP.makepasv(), which applies the same rule.
++    untrusted_host, sourceport = parse227(source.sendcmd('PASV'))
++    if source.trust_server_pasv_ipv4_address:
++        sourcehost = untrusted_host
++    else:
++        sourcehost = source.sock.getpeername()[0]
+     target.sendport(sourcehost, sourceport)
+     # RFC 959: the user must "listen" [...] BEFORE sending the
+     # transfer request.
+--- python3.13-3.13.5.orig/Lib/test/test_ftplib.py
++++ python3.13-3.13.5/Lib/test/test_ftplib.py
+@@ -16,7 +16,7 @@ try:
+ except ImportError:
+     ssl = None
+ 
+-from unittest import TestCase, skipUnless
++from unittest import mock, TestCase, skipUnless
+ from test import support
+ from test.support import requires_subprocess
+ from test.support import threading_helper
+@@ -1145,6 +1145,40 @@ class TestTimeouts(TestCase):
+         ftp.close()
+ 
+ 
++class TestFtpcpSecurity(TestCase):
++    """ftpcp() must not trust the host a source server advertises in PASV.
++
++    A malicious source server can otherwise redirect the target server's
++    data connection to an arbitrary host:port (SSRF), so ftpcp() uses the
++    source server's actual peer address instead, the same as FTP.makepasv().
++    """
++
++    def _make_pair(self, *, advertised_host, real_host, trust=False):
++        source = mock.Mock(spec=ftplib.FTP)
++        source.trust_server_pasv_ipv4_address = trust
++        source.sock.getpeername.return_value = (real_host, 21)
++        # PASV replies give the host as comma-separated octets, not dotted.
++        advertised = advertised_host.replace('.', ',')
++        source.sendcmd.side_effect = lambda cmd: (
++            f'227 Entering Passive Mode ({advertised},1,2).'
++            if cmd == 'PASV' else '150 ok')
++        target = mock.Mock(spec=ftplib.FTP)
++        target.sendcmd.return_value = '150 ok'
++        return source, target
++
++    def test_ftpcp_ignores_untrusted_pasv_host(self):
++        source, target = self._make_pair(advertised_host='10.0.0.5',
++                                         real_host='198.51.100.7')
++        ftplib.ftpcp(source, 'a', target, 'b')
++        target.sendport.assert_called_once_with('198.51.100.7', 258)
++
++    def test_ftpcp_trust_server_pasv_ipv4_address(self):
++        source, target = self._make_pair(advertised_host='10.0.0.5',
++                                         real_host='198.51.100.7', trust=True)
++        ftplib.ftpcp(source, 'a', target, 'b')
++        target.sendport.assert_called_once_with('10.0.0.5', 258)
++
++
+ class MiscTestCase(TestCase):
+     def test__all__(self):
+         not_exported = {
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-9669.patch 
python3.13-3.13.5/debian/patches/CVE-2026-9669.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-9669.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-9669.patch        2026-06-08 
22:58:08.000000000 +0200
@@ -0,0 +1,82 @@
+From 619a12b2e545391dc436b3af79dda22337382a6f Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 8 Jun 2026 11:55:32 +0200
+Subject: [PATCH] [3.13] gh-150599: Prevent bz2 decompressor reuse after errors
+ (GH-150600)
+
+--- python3.13-3.13.5.orig/Lib/test/test_bz2.py
++++ python3.13-3.13.5/Lib/test/test_bz2.py
+@@ -1022,6 +1022,21 @@ class BZ2DecompressorTest(BaseTest):
+         # Previously, a second call could crash due to internal inconsistency
+         self.assertRaises(Exception, bzd.decompress, self.BAD_DATA * 30)
+ 
++    def test_decompress_after_data_error(self):
++        data = bytes.fromhex(
++            "425a6839314159265359000000000000007fffff000000000000000000000000"
++            "00000000000000000000000000000000000000e0370000000000000000000000"
++            "000000000000000000000000000000000000000000000000000083f3"
++        )
++        bzd = BZ2Decompressor()
++        with self.assertRaisesRegex(OSError, "Invalid data stream"):
++            bzd.decompress(data)
++        # Previously, a second call could crash due to internal inconsistency
++        self.assertFalse(bzd.needs_input)
++        self.assertFalse(bzd.eof)
++        with self.assertRaisesRegex(ValueError, "previous error"):
++            bzd.decompress(b'\x00' * 18)
++
+     @support.refcount_test
+     def test_refleaks_in___init__(self):
+         gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
+--- python3.13-3.13.5.orig/Modules/_bz2module.c
++++ python3.13-3.13.5/Modules/_bz2module.c
+@@ -116,6 +116,7 @@ typedef struct {
+ typedef struct {
+     PyObject_HEAD
+     bz_stream bzs;
++    int bzerror;
+     char eof;           /* Py_T_BOOL expects a char */
+     PyObject *unused_data;
+     char needs_input;
+@@ -455,8 +456,11 @@ decompress_buf(BZ2Decompressor *d, Py_ss
+ 
+         d->bzs_avail_in_real += bzs->avail_in;
+ 
+-        if (catch_bz2_error(bzret))
++        if (catch_bz2_error(bzret)) {
++            d->bzerror = bzret;
++            d->needs_input = 0;
+             goto error;
++        }
+         if (bzret == BZ_STREAM_END) {
+             d->eof = 1;
+             break;
+@@ -624,10 +628,17 @@ _bz2_BZ2Decompressor_decompress_impl(BZ2
+     PyObject *result = NULL;
+ 
+     ACQUIRE_LOCK(self);
+-    if (self->eof)
++    if (self->eof) {
+         PyErr_SetString(PyExc_EOFError, "End of stream already reached");
+-    else
++    }
++    else if (self->bzerror) {
++        // Re-entering BZ2_bzDecompress() after an error can write out of 
bounds.
++        PyErr_SetString(PyExc_ValueError,
++                        "Decompressor is unusable after a previous error");
++    }
++    else {
+         result = decompress(self, data->buf, data->len, max_length);
++    }
+     RELEASE_LOCK(self);
+     return result;
+ }
+@@ -661,6 +672,7 @@ _bz2_BZ2Decompressor_impl(PyTypeObject *
+         return NULL;
+     }
+ 
++    self->bzerror = 0;
+     self->needs_input = 1;
+     self->bzs_avail_in_real = 0;
+     self->input_buffer = 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-05-05 23:05:32.000000000 
+0200
+++ python3.13-3.13.5/debian/patches/series     2026-06-08 22:58:08.000000000 
+0200
@@ -48,3 +48,8 @@
 CVE-2026-4519.patch
 CVE-2026-6019.patch
 CVE-2026-6100.patch
+CVE-2026-1502.patch
+CVE-2026-3276.patch
+CVE-2026-7774.patch
+CVE-2026-8328.patch
+CVE-2026-9669.patch

--- End Message ---
--- Begin Message ---
Version: 13.6

This update was released as part of 13.6.

--- End Message ---

Reply via email to