Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python310 for openSUSE:Factory 
checked in at 2026-10-01 16:46:15
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python310 (Old)
 and      /work/SRC/openSUSE:Factory/.python310.new.1253 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python310"

Thu Oct  1 16:46:15 2026 rev:83 rq:1381747 version:3.10.21

Changes:
--------
--- /work/SRC/openSUSE:Factory/python310/python310.changes      2026-09-10 
11:51:05.962856029 +0200
+++ /work/SRC/openSUSE:Factory/.python310.new.1253/python310.changes    
2026-10-01 16:47:15.795589456 +0200
@@ -1,0 +2,27 @@
+Thu Sep 17 23:49:43 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-15310: bound zipfile decompression for
+  bzip2/LZMA/Zstandard (bsc#1277111, gh#python/cpython#156002)
+  CVE-2026-15310-bound-zipfile-decompression.patch
+
+-------------------------------------------------------------------
+Tue Sep 15 10:29:32 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-19672: in tarfile, handle a member that leaves the
+  destination and comes back  (bsc#1276227, gh#python/cpython#156000)
+  CVE-2026-19672-tarfile-outside-dirs.patch
+
+-------------------------------------------------------------------
+Fri Sep 11 20:18:06 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-17084: Don't consider Unicode codepoint attributes
+  outside RFC 3454 (bsc#1276226)
+  * Add CVE-2026-17084-stringprep-rfc3454.patch
+-------------------------------------------------------------------
+Thu Sep 10 18:29:11 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-15806: Scope HTTPPasswordMgr credentials by URL scheme
+  (bsc#1276223)
+  * Add CVE-2026-15806-HTTPPasswordMgr-scheme.patch
+
+-------------------------------------------------------------------

New:
----
  CVE-2026-15310-bound-zipfile-decompression.patch
  CVE-2026-15806-HTTPPasswordMgr-scheme.patch
  CVE-2026-17084-stringprep-rfc3454.patch
  CVE-2026-19672-tarfile-outside-dirs.patch

----------(New B)----------
  New:  bzip2/LZMA/Zstandard (bsc#1277111, gh#python/cpython#156002)
  CVE-2026-15310-bound-zipfile-decompression.patch
  New:  (bsc#1276223)
  * Add CVE-2026-15806-HTTPPasswordMgr-scheme.patch
  New:  outside RFC 3454 (bsc#1276226)
  * Add CVE-2026-17084-stringprep-rfc3454.patch
-------------------------------------------------------------------
  New:  destination and comes back  (bsc#1276227, gh#python/cpython#156000)
  CVE-2026-19672-tarfile-outside-dirs.patch
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python310.spec ++++++
--- /var/tmp/diff_new_pack.frFgMc/_old  2026-10-01 16:47:17.133645536 +0200
+++ /var/tmp/diff_new_pack.frFgMc/_new  2026-10-01 16:47:17.135645620 +0200
@@ -233,6 +233,18 @@
 # PATCH-FIX-UPSTREAM bsc1263083-http-cookies-atob-utf8.patch bsc#1263083 
[email protected]
 # Use decodeURIComponent() for UTF-8 support in js_output()
 Patch63:        bsc1263083-http-cookies-atob-utf8.patch
+# PATCH-FIX-UPSTREAM CVE-2026-15806-HTTPPasswordMgr-scheme.patch bsc#1276223 
[email protected]
+# Scope HTTPPasswordMgr credentials by URL scheme
+Patch64:        CVE-2026-15806-HTTPPasswordMgr-scheme.patch
+# PATCH-FIX-UPSTREAM CVE-2026-17084-stringprep-rfc3454.patch bsc#1276226 Matej 
Cepl <[email protected]>
+# Don't consider Unicode codepoint attributes outside RFC 3454
+Patch65:        CVE-2026-17084-stringprep-rfc3454.patch
+# PATCH-FIX-UPSTREAM CVE-2026-19672-tarfile-outside-dirs.patch bsc#1276227 
[email protected]
+# in tarfile, handle a member that leaves the destination and comes back
+Patch66:        CVE-2026-19672-tarfile-outside-dirs.patch
+# PATCH-FIX-UPSTREAM CVE-2026-15310-bound-zipfile-decompression.patch 
bsc#1277111 [email protected]
+# Bound zipfile decompression for bzip2/LZMA/Zstandard 
(gh#python/cpython!156003)
+Patch67:        CVE-2026-15310-bound-zipfile-decompression.patch
 ### END OF PATCHES                                                             
 
 BuildRequires:  autoconf-archive
 BuildRequires:  automake

++++++ CVE-2026-15310-bound-zipfile-decompression.patch ++++++
>From e116414a68038c1c52e983fabc2661f074bc0387 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Mon, 31 Aug 2026 21:04:04 +0200
Subject: [PATCH 1/5] [3.15] gh-156002: Bound zipfile decompression for
 bzip2/LZMA/Zstandard (GH-156003) (GH-156362)

Patch by @tonghuaroot.

zipfile.ZipExtFile._read1() bounds the output of each decompress() call
for DEFLATE members by passing a max_length to zlib, but for bzip2, LZMA,
and Zstandard members it called decompress() with no bound. A whole
compressed chunk was therefore expanded into a single allocation before
the data[:self._left] clip ran, so a consumer that deliberately reads in
small chunks to limit memory (for example zf.open(name).read(8192)) was
silently unprotected for non-DEFLATE members. A small, spec-conformant
archive member declaring a large uncompressed size could drive multi-GB
peak memory.

_read1() now passes a per-call bound to the non-DEFLATE decompress()
(mirroring the DEFLATE branch) and drains the decompressor's internal
buffer across calls by checking needs_input before reading more
compressed input. zipfile's LZMADecompressor wrapper forwards max_length
and exposes needs_input so the bound also holds for LZMA members.

(cherry picked from commit f897dbf2f36a5935700b7c2d94d4681d2136b7d4)
(cherry picked from commit 1b424c0178a01e155fd0267dc28a8fc1159b33a8)

Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: tonghuaroot <[email protected]>
---
 Lib/test/test_zipfile.py                      | 42 +++++++++++++++++++
 Lib/zipfile.py                                | 38 ++++++++++++++---
 ...-08-18-13-54-05.gh-issue-156002.CcWXPP.rst |  4 ++
 3 files changed, 79 insertions(+), 5 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst

diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
index 55e9792b36aa6c5..de97985cb2684b6 100644
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -2198,6 +2198,48 @@ def tearDown(self):
         unlink(TESTFN2)
 
 
+class AbstractBoundedDecompressTests:
+    # ZipExtFile._read1() bounds the output of each decompress() call so that a
+    # small member declaring a large uncompressed size cannot expand into one
+    # unbounded read.
+    def test_read1_output_is_bounded(self):
+        buf = io.BytesIO()
+        with zipfile.ZipFile(buf, "w", compression=self.compression) as zf:
+            zf.writestr("big", b"\0" * (4 * 1024 * 1024))
+        with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
+            with zf.open("big") as f:
+                self.assertLessEqual(len(f._read1(100)), f.MIN_READ_SIZE)
+
+
+class StoredBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                   unittest.TestCase):
+    compression = zipfile.ZIP_STORED
+
+
+@requires_zlib()
+class DeflateBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                    unittest.TestCase):
+    compression = zipfile.ZIP_DEFLATED
+
+
+@requires_bz2()
+class Bzip2BoundedDecompressTests(AbstractBoundedDecompressTests,
+                                  unittest.TestCase):
+    compression = zipfile.ZIP_BZIP2
+
+
+@requires_lzma()
+class LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                 unittest.TestCase):
+    compression = zipfile.ZIP_LZMA
+
+
+@requires_zstd()
+class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                 unittest.TestCase):
+    compression = zipfile.ZIP_ZSTANDARD
+
+
 class AbstractBadCrcTests:
     def test_testzip_with_bad_crc(self):
         """Tests that files with bad CRCs return their name from testzip."""
diff --git a/Lib/zipfile.py b/Lib/zipfile.py
index 38bf08064cfa41c..8352dc9ab47fbe5 100644
--- a/Lib/zipfile.py
+++ b/Lib/zipfile.py
@@ -640,7 +640,16 @@ def __init__(self):
         self._unconsumed = b''
         self.eof = False
 
-    def decompress(self, data):
+    @property
+    def _needs_input(self):
+        # While the LZMA properties header is still being buffered, more input
+        # is required; afterwards defer to the wrapped decompressor so a 
bounded
+        # decompress() call can be drained across reads.
+        if self._decomp is None:
+            return True
+        return self._decomp.needs_input
+
+    def decompress(self, data, max_length=-1):
         if self._decomp is None:
             self._unconsumed += data
             if len(self._unconsumed) <= 4:
@@ -656,7 +665,7 @@ def decompress(self, data):
             data = self._unconsumed[4 + psize:]
             del self._unconsumed
 
-        result = self._decomp.decompress(data)
+        result = self._decomp.decompress(data, max_length)
         self.eof = self._decomp.eof
         return result
 
@@ -716,6 +725,13 @@ def _get_compressor(compress_type, compresslevel=None):
         return None
 
 
+def _decompressor_needs_input(decompressor):
+    # bz2/zstd expose the stdlib decompressor's public needs_input; the LZMA
+    # wrapper keeps it private (_needs_input) to avoid adding public API.
+    needs_input = getattr(decompressor, "needs_input", None)
+    return decompressor._needs_input if needs_input is None else needs_input
+
+
 def _get_decompressor(compress_type):
     _check_compression(compress_type)
     if compress_type == ZIP_STORED:
@@ -1012,8 +1028,15 @@ def _read1(self, n):
             data = self._decompressor.unconsumed_tail
             if n > len(data):
                 data += self._read2(n - len(data))
-        else:
+        elif self._compress_type == ZIP_STORED:
             data = self._read2(n)
+        else:
+            # bzip2/lzma/zstd: a bounded decompress() call may leave input
+            # buffered inside the decompressor; drain that before reading more.
+            if _decompressor_needs_input(self._decompressor):
+                data = self._read2(n)
+            else:
+                data = b''
 
         if self._compress_type == ZIP_STORED:
             self._eof = self._compress_left <= 0
@@ -1026,8 +1049,13 @@ def _read1(self, n):
             if self._eof:
                 data += self._decompressor.flush()
         else:
-            data = self._decompressor.decompress(data)
-            self._eof = self._decompressor.eof or self._compress_left <= 0
+            # Bound the output of a single decompress() call (mirroring the
+            # DEFLATE path above) so that a small compressed member cannot
+            # expand into one unbounded read.
+            data = self._decompressor.decompress(data, max(n, 
self.MIN_READ_SIZE))
+            self._eof = (self._decompressor.eof or
+                         self._compress_left <= 0 and
+                         _decompressor_needs_input(self._decompressor))
 
         data = data[:self._left]
         self._left -= len(data)
diff --git 
a/Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst 
b/Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst
new file mode 100644
index 000000000000000..4e49ad5ce8fa00a
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst
@@ -0,0 +1,4 @@
+Bound the amount of data :mod:`zipfile` decompresses per read for members
+compressed with bzip2, LZMA, or Zstandard, matching the existing limit for
+deflate. A small archive member could previously expand into an unbounded
+allocation even when read in small chunks.

>From 3696a46be8c36cf708c1f88efa03fd8331f3733b Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Thu, 3 Sep 2026 16:39:39 +0200
Subject: [PATCH 2/5] Remove zstd test (3.14+)

---
 Lib/test/test_zipfile.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
index de97985cb2684b6..6c4ba365eb0eea6 100644
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -2234,12 +2234,6 @@ class 
LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
     compression = zipfile.ZIP_LZMA
 
 
-@requires_zstd()
-class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests,
-                                 unittest.TestCase):
-    compression = zipfile.ZIP_ZSTANDARD
-
-
 class AbstractBadCrcTests:
     def test_testzip_with_bad_crc(self):
         """Tests that files with bad CRCs return their name from testzip."""

>From c6b8aabcb5c73aef402ad5d34257a7440dd09d06 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
 <[email protected]>
Date: Wed, 16 Sep 2026 06:52:44 -0700
Subject: [PATCH 3/5] gh-156002: Keep reading through monkey-patched zipfile
 decompressors (GH-157180) (GH-157557)

(cherry picked from commit f507e6946a3194e83e1d7b8ee6e14567175e46de)

Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: rasmusfaber <[email protected]>
---
 Lib/test/test_zipfile.py                      | 68 +++++++++++++++++++
 Lib/zipfile.py                                | 19 +++---
 ...-09-08-13-06-29.gh-issue-156002.vmOC8T.rst |  5 ++
 3 files changed, 81 insertions(+), 11 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst

diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
index 6c4ba365eb0eea6..1d069c156ffd402 100644
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -2234,6 +2234,74 @@ class 
LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
     compression = zipfile.ZIP_LZMA
 
 
+
+class MonkeypatchedDecompressorTests(unittest.TestCase):
+    # Some third-party projects monkey-patch _get_decompressor() to add
+    # additional compression schemes. This can break at any time as the
+    # internal compressor objects change.
+    # To protect users, we try to keep this case working.
+    # See also: GH-156002 and GH-113767.
+    COMPRESSION = 99
+
+    class Compressor:
+        """Compressor with only the original BZ2Compressor API"""
+        def compress(self, data):
+            return data.swapcase()
+
+        def flush(self):
+            return b''
+
+    class Decompressor:
+        """Decompressor with only the 3.3+ BZ2Decompressor API"""
+        eof = False
+
+        def decompress(self, data):
+            return data.swapcase()
+
+    def setUp(self):
+        orig_check_compression = zipfile._check_compression
+        orig_get_compressor = zipfile._get_compressor
+        orig_get_decompressor = zipfile._get_decompressor
+
+        def check_compression(compression):
+            if compression != self.COMPRESSION:
+                orig_check_compression(compression)
+
+        def get_compressor(compress_type, compresslevel=None):
+            if compress_type == self.COMPRESSION:
+                return self.Compressor()
+            return orig_get_compressor(compress_type, compresslevel)
+
+        def get_decompressor(compress_type):
+            if compress_type == self.COMPRESSION:
+                return self.Decompressor()
+            return orig_get_decompressor(compress_type)
+
+        self.enterContext(mock.patch.object(
+            zipfile, '_check_compression', check_compression))
+        self.enterContext(mock.patch.object(
+            zipfile, '_get_compressor', get_compressor))
+        self.enterContext(mock.patch.object(
+            zipfile, '_get_decompressor', get_decompressor))
+
+    def test_roundtrip_monkeypatched_decompressor(self):
+        data = bytes(range(256)) * 8
+        buf = io.BytesIO()
+        with zipfile.ZipFile(buf, "w", compression=self.COMPRESSION) as zf:
+            zf.writestr("member", data)
+        self.assertIn(data.swapcase(), buf.getvalue())
+        with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
+            self.assertEqual(zf.read("member"), data)
+            with zf.open("member") as f:
+                self.assertEqual(f.read(100), data[:100])
+                self.assertEqual(f.read1(100), data[100:200])
+                f.seek(-100, os.SEEK_END)
+                self.assertEqual(f.read(), data[-100:])
+                # Rewinding past the read buffer re-creates the decompressor.
+                f.seek(0)
+                self.assertEqual(f.read(), data)
+
+
 class AbstractBadCrcTests:
     def test_testzip_with_bad_crc(self):
         """Tests that files with bad CRCs return their name from testzip."""
diff --git a/Lib/zipfile.py b/Lib/zipfile.py
index 8352dc9ab47fbe5..33beef209551544 100644
--- a/Lib/zipfile.py
+++ b/Lib/zipfile.py
@@ -641,7 +641,7 @@ def __init__(self):
         self.eof = False
 
     @property
-    def _needs_input(self):
+    def needs_input(self):
         # While the LZMA properties header is still being buffered, more input
         # is required; afterwards defer to the wrapped decompressor so a 
bounded
         # decompress() call can be drained across reads.
@@ -725,13 +725,6 @@ def _get_compressor(compress_type, compresslevel=None):
         return None
 
 
-def _decompressor_needs_input(decompressor):
-    # bz2/zstd expose the stdlib decompressor's public needs_input; the LZMA
-    # wrapper keeps it private (_needs_input) to avoid adding public API.
-    needs_input = getattr(decompressor, "needs_input", None)
-    return decompressor._needs_input if needs_input is None else needs_input
-
-
 def _get_decompressor(compress_type):
     _check_compression(compress_type)
     if compress_type == ZIP_STORED:
@@ -1033,7 +1026,7 @@ def _read1(self, n):
         else:
             # bzip2/lzma/zstd: a bounded decompress() call may leave input
             # buffered inside the decompressor; drain that before reading more.
-            if _decompressor_needs_input(self._decompressor):
+            if getattr(self._decompressor, "needs_input", True):
                 data = self._read2(n)
             else:
                 data = b''
@@ -1052,10 +1045,14 @@ def _read1(self, n):
             # Bound the output of a single decompress() call (mirroring the
             # DEFLATE path above) so that a small compressed member cannot
             # expand into one unbounded read.
-            data = self._decompressor.decompress(data, max(n, 
self.MIN_READ_SIZE))
+            try:
+                data = self._decompressor.decompress(data, max(n, 
self.MIN_READ_SIZE))
+            except TypeError:
+                # See MonkeypatchedDecompressorTests in test_core.py
+                data = self._decompressor.decompress(data)
             self._eof = (self._decompressor.eof or
                          self._compress_left <= 0 and
-                         _decompressor_needs_input(self._decompressor))
+                         getattr(self._decompressor, "needs_input", True))
 
         data = data[:self._left]
         self._left -= len(data)
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst 
b/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
new file mode 100644
index 000000000000000..a21386803cca0f4
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
@@ -0,0 +1,5 @@
+:mod:`zipfile` again reads members through a third-party decompressor
+installed by monkey-patching the private ``_get_decompressor()`` to return an
+object that only implements old BZ2Decompressor API from Python 3.3.
+Note that decompressors without ``needs_input`` and two-argument
+``decompress()`` are vulnerable to :cve:`2026-15310`.

>From 14bfd82b3aae6218263d3f4057d292f64200a247 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Wed, 16 Sep 2026 18:08:46 +0200
Subject: [PATCH 4/5] Avoid unittest's enterContext; it doesn't exist yet

---
 Lib/test/test_zipfile.py | 45 +++++++++++++++++++---------------------
 1 file changed, 21 insertions(+), 24 deletions(-)

diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
index 1d069c156ffd402..fea72705d7fd19c 100644
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -2258,7 +2258,7 @@ class Decompressor:
         def decompress(self, data):
             return data.swapcase()
 
-    def setUp(self):
+    def test_roundtrip_monkeypatched_decompressor(self):
         orig_check_compression = zipfile._check_compression
         orig_get_compressor = zipfile._get_compressor
         orig_get_decompressor = zipfile._get_decompressor
@@ -2277,29 +2277,26 @@ def get_decompressor(compress_type):
                 return self.Decompressor()
             return orig_get_decompressor(compress_type)
 
-        self.enterContext(mock.patch.object(
-            zipfile, '_check_compression', check_compression))
-        self.enterContext(mock.patch.object(
-            zipfile, '_get_compressor', get_compressor))
-        self.enterContext(mock.patch.object(
-            zipfile, '_get_decompressor', get_decompressor))
-
-    def test_roundtrip_monkeypatched_decompressor(self):
-        data = bytes(range(256)) * 8
-        buf = io.BytesIO()
-        with zipfile.ZipFile(buf, "w", compression=self.COMPRESSION) as zf:
-            zf.writestr("member", data)
-        self.assertIn(data.swapcase(), buf.getvalue())
-        with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
-            self.assertEqual(zf.read("member"), data)
-            with zf.open("member") as f:
-                self.assertEqual(f.read(100), data[:100])
-                self.assertEqual(f.read1(100), data[100:200])
-                f.seek(-100, os.SEEK_END)
-                self.assertEqual(f.read(), data[-100:])
-                # Rewinding past the read buffer re-creates the decompressor.
-                f.seek(0)
-                self.assertEqual(f.read(), data)
+        with (
+            mock.patch.object(zipfile, '_check_compression', 
check_compression),
+            mock.patch.object(zipfile, '_get_compressor', get_compressor),
+            mock.patch.object(zipfile, '_get_decompressor', get_decompressor),
+        ):
+            data = bytes(range(256)) * 8
+            buf = io.BytesIO()
+            with zipfile.ZipFile(buf, "w", compression=self.COMPRESSION) as zf:
+                zf.writestr("member", data)
+            self.assertIn(data.swapcase(), buf.getvalue())
+            with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
+                self.assertEqual(zf.read("member"), data)
+                with zf.open("member") as f:
+                    self.assertEqual(f.read(100), data[:100])
+                    self.assertEqual(f.read1(100), data[100:200])
+                    f.seek(-100, os.SEEK_END)
+                    self.assertEqual(f.read(), data[-100:])
+                    # Rewinding past the read buffer re-creates the 
decompressor.
+                    f.seek(0)
+                    self.assertEqual(f.read(), data)
 
 
 class AbstractBadCrcTests:

>From 42b6c97424c4d9b4472640eac7d49026a733edd4 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Wed, 16 Sep 2026 18:10:58 +0200
Subject: [PATCH 5/5] Don't use the :cve: RST role, it doesn't exist yet

---
 .../next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git 
a/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst 
b/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
index a21386803cca0f4..689967bff72fe7c 100644
--- a/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
+++ b/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
@@ -2,4 +2,4 @@
 installed by monkey-patching the private ``_get_decompressor()`` to return an
 object that only implements old BZ2Decompressor API from Python 3.3.
 Note that decompressors without ``needs_input`` and two-argument
-``decompress()`` are vulnerable to :cve:`2026-15310`.
+``decompress()`` are vulnerable to CVE 2026-15310.

++++++ CVE-2026-15806-HTTPPasswordMgr-scheme.patch ++++++
>From 95e57c3cfe9b6f9a9e70d067afeeff16aaa503b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz?= <[email protected]>
Date: Mon, 17 Aug 2026 21:39:16 +0200
Subject: [PATCH 1/3] gh-155694: Scope HTTPPasswordMgr credentials by URL
 scheme (GH-155696)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Credentials stored for an https:// URI were also matched against the
corresponding http:// URI, since `reduce_uri()` discards the scheme.

`HTTPPasswordMgr` and `HTTPPasswordMgrWithPriorAuth` now compare the scheme
too; URIs registered without a scheme still match any scheme.
(cherry picked from commit a7bb524fef61f77ede01f660ffbd591e1d5837ce)

Co-authored-by: Ɓukasz <[email protected]>
---
 Doc/library/urllib.request.rst                                           |   
10 +
 Lib/test/test_urllib2.py                                                 |   
56 ++++++++++
 Lib/urllib/request.py                                                    |   
25 +++-
 Misc/NEWS.d/next/Security/2026-07-31-16-20-17.gh-issue-155694.SsxlKG.rst |    
4 
 4 files changed, 87 insertions(+), 8 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-07-31-16-20-17.gh-issue-155694.SsxlKG.rst

Index: Python-3.10.21/Doc/library/urllib.request.rst
===================================================================
--- Python-3.10.21.orig/Doc/library/urllib.request.rst  2026-08-13 
01:03:10.000000000 +0200
+++ Python-3.10.21/Doc/library/urllib.request.rst       2026-09-10 
23:41:06.501393971 +0200
@@ -922,8 +922,14 @@
 
    *uri* can be either a single URI, or a sequence of URIs. *realm*, *user* and
    *passwd* must be strings. This causes ``(user, passwd)`` to be used as
-   authentication tokens when authentication for *realm* and a super-URI of 
any of
-   the given URIs is given.
+   authentication tokens when authentication for *realm* and a super-URI of any
+   of the given URIs is given. If a URI includes a scheme, its credentials only
+   match authentication URIs with the same scheme or no scheme. A URI without a
+   scheme matches authentication URIs with any scheme.
+
+   .. versionchanged:: next
+      Authentication credentials for URIs with a scheme are now scoped by
+      that scheme.
 
 
 .. method:: HTTPPasswordMgr.find_user_password(realm, authuri)
Index: Python-3.10.21/Lib/test/test_urllib2.py
===================================================================
--- Python-3.10.21.orig/Lib/test/test_urllib2.py        2026-09-10 
23:41:03.305710092 +0200
+++ Python-3.10.21/Lib/test/test_urllib2.py     2026-09-10 23:41:06.502157280 
+0200
@@ -269,6 +269,50 @@
         self.assertEqual(find_user_pass("i", "http://j.example.com:80";),
                          (None, None))
 
+    def test_password_manager_scheme(self):
+        mgr = urllib.request.HTTPPasswordMgr()
+        mgr.add_password(
+            "realm", "https://example.com/";, "user", "password")
+
+        self.assertEqual(
+            mgr.find_user_password("realm", "https://example.com/";),
+            ("user", "password"))
+        self.assertEqual(
+            mgr.find_user_password("realm", "http://example.com/";),
+            (None, None))
+        # Support an authority without a scheme.
+        self.assertEqual(
+            mgr.find_user_password("realm", "example.com"),
+            ("user", "password"))
+        # An authority without a scheme continues to match any scheme.
+        mgr.add_password(
+            "realm", "schemeless.example.com", "user", "password")
+        for scheme in "http", "https":
+            with self.subTest(scheme=scheme):
+                self.assertEqual(
+                    mgr.find_user_password(
+                        "realm", f"{scheme}://schemeless.example.com/"),
+                    ("user", "password"))
+
+        # A network-path reference also has no scheme.
+        mgr.add_password(
+            "realm", "//network-path.example.com/", "user", "password")
+        self.assertEqual(
+            mgr.find_user_password(
+                "realm", "https://network-path.example.com/";),
+            ("user", "password"))
+
+    def test_password_manager_reduced_uri(self):
+        mgr = urllib.request.HTTPPasswordMgr()
+
+        self.assertEqual(
+            mgr.reduce_uri("http://example.com/path";),
+            ("example.com:80", "/path"))
+        self.assertTrue(
+            mgr.is_suburi(
+                ("example.com", "/path"),
+                ("example.com", "/path/subpath")))
+
 
 class MockOpener:
     addheaders = []
@@ -1712,6 +1756,18 @@
         # expect request to be sent with auth header
         self.assertTrue(http_handler.has_auth_header)
 
+    def test_basic_prior_auth_different_scheme(self):
+        pwd_manager = HTTPPasswordMgrWithPriorAuth()
+        auth_handler = HTTPBasicAuthHandler(pwd_manager)
+        auth_handler.add_password(
+            None, "https://example.com/";, "user", "password",
+            is_authenticated=True)
+
+        request = Request("http://example.com/";)
+        auth_handler.http_request(request)
+
+        self.assertFalse(request.has_header("Authorization"))
+
     def test_basic_prior_auth_send_after_first_success(self):
         # Auto send auth header after authentication is successful once
 
Index: Python-3.10.21/Lib/urllib/request.py
===================================================================
--- Python-3.10.21.orig/Lib/urllib/request.py   2026-09-10 23:41:06.370374750 
+0200
+++ Python-3.10.21/Lib/urllib/request.py        2026-09-10 23:41:06.502739077 
+0200
@@ -844,16 +844,17 @@
             self.passwd[realm] = {}
         for default_port in True, False:
             reduced_uri = tuple(
-                self.reduce_uri(u, default_port) for u in uri)
+                self._reduce_uri_with_scheme(u, default_port) for u in uri)
             self.passwd[realm][reduced_uri] = (user, passwd)
 
     def find_user_password(self, realm, authuri):
         domains = self.passwd.get(realm, {})
         for default_port in True, False:
-            reduced_authuri = self.reduce_uri(authuri, default_port)
+            reduced_authuri = self._reduce_uri_with_scheme(
+                authuri, default_port)
             for uris, authinfo in domains.items():
                 for uri in uris:
-                    if self.is_suburi(uri, reduced_authuri):
+                    if self._is_suburi_with_scheme(uri, reduced_authuri):
                         return authinfo
         return None, None
 
@@ -880,6 +881,17 @@
                 authority = "%s:%d" % (host, dport)
         return authority, path
 
+    def _reduce_uri_with_scheme(self, uri, default_port=True):
+        parts = urlsplit(uri)
+        scheme = parts[0] if parts[1] else None
+        return (scheme or None, *self.reduce_uri(uri, default_port))
+
+    def _is_suburi_with_scheme(self, base, test):
+        if (base[0] is not None and test[0] is not None and
+                base[0] != test[0]):
+            return False
+        return self.is_suburi(base[1:], test[1:])
+
     def is_suburi(self, base, test):
         """Check if test is below base in a URI tree
 
@@ -925,14 +937,15 @@
 
         for default_port in True, False:
             for u in uri:
-                reduced_uri = self.reduce_uri(u, default_port)
+                reduced_uri = self._reduce_uri_with_scheme(u, default_port)
                 self.authenticated[reduced_uri] = is_authenticated
 
     def is_authenticated(self, authuri):
         for default_port in True, False:
-            reduced_authuri = self.reduce_uri(authuri, default_port)
+            reduced_authuri = self._reduce_uri_with_scheme(
+                authuri, default_port)
             for uri in self.authenticated:
-                if self.is_suburi(uri, reduced_authuri):
+                if self._is_suburi_with_scheme(uri, reduced_authuri):
                     return self.authenticated[uri]
 
 
Index: 
Python-3.10.21/Misc/NEWS.d/next/Security/2026-07-31-16-20-17.gh-issue-155694.SsxlKG.rst
===================================================================
--- /dev/null   1970-01-01 00:00:00.000000000 +0000
+++ 
Python-3.10.21/Misc/NEWS.d/next/Security/2026-07-31-16-20-17.gh-issue-155694.SsxlKG.rst
     2026-09-10 23:41:06.503190077 +0200
@@ -0,0 +1,4 @@
+Fix `CVE-2026-15806 <https://www.cve.org/CVERecord?id=CVE-2026-15806>`_ by 
scoping :class:`~urllib.request.HTTPPasswordMgr`
+credentials to the URL scheme, preventing credentials stored for an HTTPS
+URL from being used for a matching HTTP URL, while URIs without a scheme
+continue to match any scheme.

++++++ CVE-2026-17084-stringprep-rfc3454.patch ++++++
++++ 948 lines (skipped)

++++++ CVE-2026-19672-tarfile-outside-dirs.patch ++++++
>From 18dfe068776daa57fe031d0ff698617c78eae41c Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <[email protected]>
Date: Wed, 19 Aug 2026 09:52:01 +0100
Subject: [PATCH] [3.10] gh-155999: `tarfile`: handle a member that leaves the
 destination but comes back (GH-156000) (cherry picked from commit
 97688346ada2df3e5b9c279348862c3d64ab0823)

Co-authored-by: Stan Ulbrych <[email protected]>
---
 Doc/library/tarfile.rst                                                  |    
8 +++++
 Lib/tarfile.py                                                           |    
7 +++++
 Lib/test/test_tarfile.py                                                 |   
14 ++++++++++
 Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst |    
5 +++
 4 files changed, 34 insertions(+)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst

Index: Python-3.10.21/Doc/library/tarfile.rst
===================================================================
--- Python-3.10.21.orig/Doc/library/tarfile.rst 2026-08-13 01:03:10.000000000 
+0200
+++ Python-3.10.21/Doc/library/tarfile.rst      2026-09-15 15:56:31.165426394 
+0200
@@ -950,6 +950,10 @@
     paths (in case the name is absolute
     even after stripping slashes, e.g. ``C:/foo`` on Windows).
     This raises :class:`~tarfile.AbsolutePathError`.
+  - Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
+    using :func:`os.path.normpath`.
+    Note that this removes internal ``..`` components, which may change the
+    meaning of the name if it traverses symbolic links.
   - :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
     path (after following symlinks) would end up outside the destination.
     This raises :class:`~tarfile.OutsideDestinationError`.
@@ -958,6 +962,10 @@
 
   Return the modified ``TarInfo`` member.
 
+  .. versionchanged:: next
+
+     Filenames containing ``..`` components are now normalized.
+
 .. function:: data_filter(/, member, path)
 
   Implements the ``'data'`` filter.
Index: Python-3.10.21/Lib/tarfile.py
===================================================================
--- Python-3.10.21.orig/Lib/tarfile.py  2026-09-15 15:56:24.404294125 +0200
+++ Python-3.10.21/Lib/tarfile.py       2026-09-15 15:56:31.165790722 +0200
@@ -776,6 +776,13 @@
         # For example, 'C:/foo' on Windows.
         raise AbsolutePathError(member)
     # Ensure we stay in the destination
+    if '..' in name.replace(os.sep, '/').split('/'):
+        # Directories are created from the name as given, so a name that
+        # leaves the destination part-way through would create them
+        # outside it even if the resolved path stays inside.
+        normalized = os.path.normpath(name)
+        if normalized != name:
+            name = new_attrs['name'] = normalized
     target_path = os.path.realpath(os.path.join(dest_path, name),
                                    strict=os.path.ALLOW_MISSING)
     if os.path.commonpath([target_path, dest_path]) != dest_path:
Index: Python-3.10.21/Lib/test/test_tarfile.py
===================================================================
--- Python-3.10.21.orig/Lib/test/test_tarfile.py        2026-09-15 
15:56:25.962481162 +0200
+++ Python-3.10.21/Lib/test/test_tarfile.py     2026-09-15 15:56:31.166455367 
+0200
@@ -3535,6 +3535,20 @@
                         tarfile.AbsolutePathError,
                         """['"].*escaped.evil['"] has an absolute path""")
 
+    def test_parent_dir_out_and_back(self):
+        # Test a member that leaves the destination and comes back.
+        # The containment check looks at the resolved path, which stays
+        # inside, but the intermediate directories are created from the
+        # name as given, which does not.
+        with ArchiveMaker() as arc:
+            arc.add(f'../escaped.evil/../{self.destdir.name}/sub/file',
+                    content='content')
+
+        for filter in 'tar', 'data':
+            with self.subTest(filter):
+                with self.check_context(arc.open(), filter):
+                    self.expect_file('sub/file', content='content')
+
     def test_parent_symlink(self):
         # Test interplaying symlinks
         # Inspired by 'dirsymlink2a' in jwilk/traversal-archives
Index: 
Python-3.10.21/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst
===================================================================
--- /dev/null   1970-01-01 00:00:00.000000000 +0000
+++ 
Python-3.10.21/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst
     2026-09-15 15:56:31.167223859 +0200
@@ -0,0 +1,5 @@
+Fix the :mod:`tarfile` ``tar`` and ``data`` extraction filters creating
+directories outside the destination for members whose name leaves the
+destination and returns to it, such as ``../evil/../dest/sub/file``. The
+containment check used the resolved path, but intermediate directories were
+created from the name as given.

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.frFgMc/_old  2026-10-01 16:47:17.334653961 +0200
+++ /var/tmp/diff_new_pack.frFgMc/_new  2026-10-01 16:47:17.338654129 +0200
@@ -1,6 +1,6 @@
-mtime: 1788936080
-commit: 5ac7890be8a1b433c6e1437f48b318684ebe7821d015bcc5e050b25eec297bef
+mtime: 1789741924
+commit: 8e25bf4dd8802cd9baae7d8d418daa809ba1acc9d825b4d663d35fb4892e11a6
 url: https://src.opensuse.org/python-interpreters/python310
-revision: 5ac7890be8a1b433c6e1437f48b318684ebe7821d015bcc5e050b25eec297bef
+revision: 8e25bf4dd8802cd9baae7d8d418daa809ba1acc9d825b4d663d35fb4892e11a6
 projectscmsync: https://src.opensuse.org/python-interpreters/_ObsPrj
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-09-18 16:32:04.000000000 +0200
@@ -0,0 +1,7 @@
+.osc
+*.obscpio
+*.osc
+_build.*
+.pbuild
+*.rej
+python310*-build/

Reply via email to