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

