diff --git a/CVE-2025-4435.patch b/CVE-2025-4435.patch index 4c2e770bc7ed73b478f32062d622c589e55610d1..723b32676a527e35b833c9b009fbecf1e57d2a40 100644 --- a/CVE-2025-4435.patch +++ b/CVE-2025-4435.patch @@ -1,7 +1,42 @@ -diff -urpN Python-3.12.2.orig/Doc/library/os.path.rst Python-3.12.2/Doc/library/os.path.rst ---- Python-3.12.2.orig/Doc/library/os.path.rst 2024-10-15 11:25:54.734232191 +0800 -+++ Python-3.12.2/Doc/library/os.path.rst 2025-06-30 17:11:47.307186910 +0800 -@@ -379,10 +379,26 @@ the :mod:`glob` module.) +From 19de092debb3d7e832e5672cc2f7b788d35951da Mon Sep 17 00:00:00 2001 +From: "T. Wouters" +Date: Tue, 3 Jun 2025 16:00:21 +0200 +Subject: [PATCH] [3.12] gh-135034: Normalize link targets in tarfile, add + `os.path.realpath(strict='allow_missing')` (GH-135037) (GH-135066) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Addresses CVEs 2024-12718, 2025-4138, 2025-4330, and 2025-4517. + +(cherry picked from commit 3612d8f51741b11f36f8fb0494d79086bac9390a) + +Co-authored-by: Łukasz Langa +Signed-off-by: Łukasz Langa +Co-authored-by: Petr Viktorin +Co-authored-by: Seth Michael Larson +Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> +Co-authored-by: Serhiy Storchaka +--- + Doc/library/os.path.rst | 33 +- + Doc/library/tarfile.rst | 20 ++ + Doc/whatsnew/3.12.rst | 34 ++ + Lib/genericpath.py | 11 +- + Lib/ntpath.py | 37 ++- + Lib/posixpath.py | 15 +- + Lib/tarfile.py | 161 +++++++-- + Lib/test/test_ntpath.py | 284 ++++++++++++++-- + Lib/test/test_posixpath.py | 289 +++++++++++++--- + Lib/test/test_tarfile.py | 313 ++++++++++++++++-- + ...-06-02-11-32-23.gh-issue-135034.RLGjbp.rst | 6 + + 11 files changed, 1064 insertions(+), 139 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst + +diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst +index 51e89087e7fa54..f5e684f2ccf4e4 100644 +--- a/Doc/library/os.path.rst ++++ b/Doc/library/os.path.rst +@@ -377,10 +377,26 @@ the :mod:`glob` module.) links encountered in the path (if they are supported by the operating system). @@ -32,7 +67,7 @@ diff -urpN Python-3.12.2.orig/Doc/library/os.path.rst Python-3.12.2/Doc/library/ .. note:: This function emulates the operating system's procedure for making a path -@@ -401,6 +417,15 @@ the :mod:`glob` module.) +@@ -399,6 +415,15 @@ the :mod:`glob` module.) .. versionchanged:: 3.10 The *strict* parameter was added. @@ -48,10 +83,11 @@ diff -urpN Python-3.12.2.orig/Doc/library/os.path.rst Python-3.12.2/Doc/library/ .. function:: relpath(path, start=os.curdir) -diff -urpN Python-3.12.2.orig/Doc/library/tarfile.rst Python-3.12.2/Doc/library/tarfile.rst ---- Python-3.12.2.orig/Doc/library/tarfile.rst 2024-10-15 11:25:54.738232215 +0800 -+++ Python-3.12.2/Doc/library/tarfile.rst 2025-06-30 17:11:47.308186916 +0800 -@@ -249,6 +249,15 @@ The :mod:`tarfile` module defines the fo +diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst +index 0352cddb16e9fd..a2403b70e29459 100644 +--- a/Doc/library/tarfile.rst ++++ b/Doc/library/tarfile.rst +@@ -249,6 +249,15 @@ The :mod:`tarfile` module defines the following exceptions: Raised to refuse extracting a symbolic link pointing outside the destination directory. @@ -91,7 +127,7 @@ diff -urpN Python-3.12.2.orig/Doc/library/tarfile.rst Python-3.12.2/Doc/library/ .. _tarfile-extraction-refuse: -@@ -1093,6 +1112,7 @@ Here is an incomplete list of things to +@@ -1093,6 +1112,7 @@ Here is an incomplete list of things to consider: * Extract to a :func:`new temporary directory ` to prevent e.g. exploiting pre-existing links, and to make it easier to clean up after a failed extraction. @@ -99,10 +135,11 @@ diff -urpN Python-3.12.2.orig/Doc/library/tarfile.rst Python-3.12.2/Doc/library/ * When working with untrusted data, use external (e.g. OS-level) limits on disk, memory and CPU usage. * Check filenames against an allow-list of characters -diff -urpN Python-3.12.2.orig/Lib/genericpath.py Python-3.12.2/Lib/genericpath.py ---- Python-3.12.2.orig/Lib/genericpath.py 2024-10-15 11:25:54.773232422 +0800 -+++ Python-3.12.2/Lib/genericpath.py 2025-06-30 17:11:47.308186916 +0800 -@@ -8,7 +8,7 @@ import stat +diff --git a/Lib/genericpath.py b/Lib/genericpath.py +index 1bd5b3897c3af9..233f7a3c67d110 100644 +--- a/Lib/genericpath.py ++++ b/Lib/genericpath.py +@@ -8,7 +8,7 @@ __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', 'getsize', 'isdir', 'isfile', 'islink', 'samefile', 'sameopenfile', @@ -124,10 +161,11 @@ diff -urpN Python-3.12.2.orig/Lib/genericpath.py Python-3.12.2/Lib/genericpath.p + return 'os.path.ALLOW_MISSING' + def __reduce__(self): + return self.__class__.__name__ -diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py ---- Python-3.12.2.orig/Lib/ntpath.py 2024-10-15 11:25:54.788232511 +0800 -+++ Python-3.12.2/Lib/ntpath.py 2025-06-30 17:11:47.308186916 +0800 -@@ -30,7 +30,8 @@ __all__ = ["normcase","isabs","join","sp +diff --git a/Lib/ntpath.py b/Lib/ntpath.py +index c05e965fcb91d1..1bef630bd9c0ff 100644 +--- a/Lib/ntpath.py ++++ b/Lib/ntpath.py +@@ -30,7 +30,8 @@ "ismount", "expanduser","expandvars","normpath","abspath", "curdir","pardir","sep","pathsep","defpath","altsep", "extsep","devnull","realpath","supports_unicode_filenames","relpath", @@ -137,7 +175,7 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py def _get_bothseps(path): if isinstance(path, bytes): -@@ -604,9 +605,10 @@ try: +@@ -609,9 +610,10 @@ def abspath(path): from nt import _getfinalpathname, readlink as _nt_readlink except ImportError: # realpath is a no-op on systems without _getfinalpathname support. @@ -150,7 +188,7 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py # These error codes indicate that we should stop reading links and # return the path we currently have. # 1: ERROR_INVALID_FUNCTION -@@ -639,7 +641,7 @@ else: +@@ -644,7 +646,7 @@ def _readlink_deep(path): path = old_path break path = normpath(join(dirname(old_path), path)) @@ -159,7 +197,7 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py if ex.winerror in allowed_winerror: break raise -@@ -648,7 +650,7 @@ else: +@@ -653,7 +655,7 @@ def _readlink_deep(path): break return path @@ -168,7 +206,7 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py # These error codes indicate that we should stop resolving the path # and return the value we currently have. # 1: ERROR_INVALID_FUNCTION -@@ -675,17 +677,18 @@ else: +@@ -680,17 +682,18 @@ def _getfinalpathname_nonstrict(path): try: path = _getfinalpathname(path) return join(path, tail) if tail else path @@ -190,7 +228,7 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py # If we fail to readlink(), let's keep traversing pass path, name = split(path) -@@ -716,6 +719,15 @@ else: +@@ -721,6 +724,15 @@ def realpath(path, *, strict=False): if normcase(path) == normcase(devnull): return '\\\\.\\NUL' had_prefix = path.startswith(prefix) @@ -206,7 +244,7 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py if not had_prefix and not isabs(path): path = join(cwd, path) try: -@@ -723,17 +735,16 @@ else: +@@ -728,17 +740,16 @@ def realpath(path, *, strict=False): initial_winerror = 0 except ValueError as ex: # gh-106242: Raised for embedded null characters @@ -228,10 +266,11 @@ diff -urpN Python-3.12.2.orig/Lib/ntpath.py Python-3.12.2/Lib/ntpath.py # The path returned by _getfinalpathname will always start with \\?\ - # strip off that prefix unless it was already provided on the original # path. -diff -urpN Python-3.12.2.orig/Lib/posixpath.py Python-3.12.2/Lib/posixpath.py ---- Python-3.12.2.orig/Lib/posixpath.py 2024-10-15 11:25:54.789232517 +0800 -+++ Python-3.12.2/Lib/posixpath.py 2025-06-30 17:11:47.308186916 +0800 -@@ -35,7 +35,7 @@ __all__ = ["normcase","isabs","join","sp +diff --git a/Lib/posixpath.py b/Lib/posixpath.py +index f1e4237b3aa0e4..90a6f545f90529 100644 +--- a/Lib/posixpath.py ++++ b/Lib/posixpath.py +@@ -35,7 +35,7 @@ "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", "devnull","realpath","supports_unicode_filenames","relpath", @@ -240,7 +279,7 @@ diff -urpN Python-3.12.2.orig/Lib/posixpath.py Python-3.12.2/Lib/posixpath.py def _get_sep(path): -@@ -446,6 +446,15 @@ def _joinrealpath(path, rest, strict, se +@@ -438,6 +438,15 @@ def _joinrealpath(path, rest, strict, seen): sep = '/' curdir = '.' pardir = '..' @@ -256,7 +295,7 @@ diff -urpN Python-3.12.2.orig/Lib/posixpath.py Python-3.12.2/Lib/posixpath.py if isabs(rest): rest = rest[1:] -@@ -468,9 +477,7 @@ def _joinrealpath(path, rest, strict, se +@@ -460,9 +469,7 @@ def _joinrealpath(path, rest, strict, seen): newpath = join(path, name) try: st = os.lstat(newpath) @@ -267,10 +306,11 @@ diff -urpN Python-3.12.2.orig/Lib/posixpath.py Python-3.12.2/Lib/posixpath.py is_link = False else: is_link = stat.S_ISLNK(st.st_mode) -diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py ---- Python-3.12.2.orig/Lib/tarfile.py 2024-10-15 11:25:54.792232535 +0800 -+++ Python-3.12.2/Lib/tarfile.py 2025-06-30 17:11:47.308186916 +0800 -@@ -752,10 +752,22 @@ class LinkOutsideDestinationError(Filter +diff --git a/Lib/tarfile.py b/Lib/tarfile.py +index 0a0f31eca06c04..9999a99d54d8b9 100755 +--- a/Lib/tarfile.py ++++ b/Lib/tarfile.py +@@ -752,10 +752,22 @@ def __init__(self, tarinfo, path): super().__init__(f'{tarinfo.name!r} would link to {path!r}, ' + 'which is outside the destination') @@ -294,7 +334,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py # Strip leading / (tar's directory separator) from filenames. # Include os.sep (target OS directory separator) as well. if name.startswith(('/', os.sep)): -@@ -765,7 +777,8 @@ def _get_filtered_attrs(member, dest_pat +@@ -765,7 +777,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True): # For example, 'C:/foo' on Windows. raise AbsolutePathError(member) # Ensure we stay in the destination @@ -304,7 +344,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py if os.path.commonpath([target_path, dest_path]) != dest_path: raise OutsideDestinationError(member, target_path) # Limit permissions (no high bits, and go-w) -@@ -803,6 +816,9 @@ def _get_filtered_attrs(member, dest_pat +@@ -803,6 +816,9 @@ def _get_filtered_attrs(member, dest_path, for_data=True): if member.islnk() or member.issym(): if os.path.isabs(member.linkname): raise AbsoluteLinkError(member) @@ -314,7 +354,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py if member.issym(): target_path = os.path.join(dest_path, os.path.dirname(name), -@@ -810,7 +826,8 @@ def _get_filtered_attrs(member, dest_pat +@@ -810,7 +826,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True): else: target_path = os.path.join(dest_path, member.linkname) @@ -324,7 +364,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py if os.path.commonpath([target_path, dest_path]) != dest_path: raise LinkOutsideDestinationError(member, target_path) return new_attrs -@@ -2258,30 +2275,58 @@ class TarFile(object): +@@ -2291,30 +2308,58 @@ def extractall(self, path=".", members=None, *, numeric_owner=False, members = self for member in members: @@ -388,7 +428,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, filter=None): """Extract a member from the archive to the current working directory, -@@ -2297,41 +2342,56 @@ class TarFile(object): +@@ -2330,41 +2375,56 @@ def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, String names of common filters are accepted. """ filter_function = self._get_filter_function(filter) @@ -438,13 +478,13 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py + filtered = copy.copy(filtered) + filtered._link_target = os.path.join(path, filtered.linkname) + return filtered, unfiltered -+ -+ def _extract_one(self, tarinfo, path, set_attrs, numeric_owner, -+ filter_function=None): -+ """Extract from filtered tarinfo to disk. - def _extract_one(self, tarinfo, path, set_attrs, numeric_owner): - """Extract from filtered tarinfo to disk""" ++ def _extract_one(self, tarinfo, path, set_attrs, numeric_owner, ++ filter_function=None): ++ """Extract from filtered tarinfo to disk. ++ + filter_function is only used when extracting a *different* + member (e.g. as fallback to creating a symlink) + """ @@ -460,7 +500,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py except OSError as e: self._handle_fatal_error(e) except ExtractError as e: -@@ -2389,9 +2449,13 @@ class TarFile(object): +@@ -2422,9 +2482,13 @@ def extractfile(self, member): return None def _extract_member(self, tarinfo, targetpath, set_attrs=True, @@ -476,7 +516,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py """ # Fetch the TarInfo object for the given name # and build the destination pathname, replacing -@@ -2420,7 +2484,10 @@ class TarFile(object): +@@ -2453,7 +2517,10 @@ def _extract_member(self, tarinfo, targetpath, set_attrs=True, elif tarinfo.ischr() or tarinfo.isblk(): self.makedev(tarinfo, targetpath) elif tarinfo.islnk() or tarinfo.issym(): @@ -488,7 +528,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py elif tarinfo.type not in SUPPORTED_TYPES: self.makeunknown(tarinfo, targetpath) else: -@@ -2503,10 +2570,18 @@ class TarFile(object): +@@ -2536,10 +2603,18 @@ def makedev(self, tarinfo, targetpath): os.makedev(tarinfo.devmajor, tarinfo.devminor)) def makelink(self, tarinfo, targetpath): @@ -507,7 +547,7 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py try: # For systems that support symbolic and hard links. if tarinfo.issym(): -@@ -2514,18 +2589,38 @@ class TarFile(object): +@@ -2547,18 +2622,38 @@ def makelink(self, tarinfo, targetpath): # Avoid FileExistsError on following os.symlink. os.unlink(targetpath) os.symlink(tarinfo.linkname, targetpath) @@ -553,10 +593,11 @@ diff -urpN Python-3.12.2.orig/Lib/tarfile.py Python-3.12.2/Lib/tarfile.py def chown(self, tarinfo, targetpath, numeric_owner): """Set owner of targetpath according to tarinfo. If numeric_owner -diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/test_ntpath.py ---- Python-3.12.2.orig/Lib/test/test_ntpath.py 2024-10-15 11:25:54.850232879 +0800 -+++ Python-3.12.2/Lib/test/test_ntpath.py 2025-06-30 17:11:47.309186922 +0800 -@@ -2,9 +2,11 @@ import inspect +diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py +index 4924db983b568f..93d701128453c9 100644 +--- a/Lib/test/test_ntpath.py ++++ b/Lib/test/test_ntpath.py +@@ -2,9 +2,11 @@ import ntpath import os import string @@ -596,7 +637,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes class NtpathTestCase(unittest.TestCase): def assertPathEqual(self, path1, path2): if path1 == path2 or _norm(path1) == _norm(path2): -@@ -363,6 +386,27 @@ class TestNtpath(NtpathTestCase): +@@ -363,6 +386,27 @@ def test_realpath_curdir(self): tester("ntpath.realpath('.\\.')", expected) tester("ntpath.realpath('\\'.join(['.'] * 100))", expected) @@ -624,7 +665,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes def test_realpath_pardir(self): expected = ntpath.normpath(os.getcwd()) tester("ntpath.realpath('..')", ntpath.dirname(expected)) -@@ -375,28 +419,59 @@ class TestNtpath(NtpathTestCase): +@@ -375,28 +419,59 @@ def test_realpath_pardir(self): tester("ntpath.realpath('\\'.join(['..'] * 50))", ntpath.splitdrive(expected)[0] + '\\') @@ -690,7 +731,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') -@@ -408,19 +483,77 @@ class TestNtpath(NtpathTestCase): +@@ -408,19 +483,77 @@ def test_realpath_strict(self): self.addCleanup(os_helper.unlink, ABSTFN) self.assertRaises(FileNotFoundError, ntpath.realpath, ABSTFN, strict=True) self.assertRaises(FileNotFoundError, ntpath.realpath, ABSTFN + "2", strict=True) @@ -771,7 +812,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') -@@ -572,7 +705,62 @@ class TestNtpath(NtpathTestCase): +@@ -572,7 +705,62 @@ def test_realpath_symlink_loops_strict(self): @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @@ -835,7 +876,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes ABSTFN = ntpath.abspath(os_helper.TESTFN) self.addCleanup(os_helper.unlink, ABSTFN + "3") self.addCleanup(os_helper.unlink, "\\\\?\\" + ABSTFN + "3.") -@@ -587,9 +775,9 @@ class TestNtpath(NtpathTestCase): +@@ -587,9 +775,9 @@ def test_realpath_symlink_prefix(self): f.write(b'1') os.symlink("\\\\?\\" + ABSTFN + "3.", ABSTFN + "3.link") @@ -847,7 +888,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes "\\\\?\\" + ABSTFN + "3.") # Resolved paths should be usable to open target files -@@ -599,14 +787,17 @@ class TestNtpath(NtpathTestCase): +@@ -599,14 +787,17 @@ def test_realpath_symlink_prefix(self): self.assertEqual(f.read(), b'1') # When the prefix is included, it is not stripped @@ -867,7 +908,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname') -@@ -630,12 +821,65 @@ class TestNtpath(NtpathTestCase): +@@ -630,12 +821,65 @@ def test_realpath_cwd(self): self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short)) @@ -939,10 +980,11 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_ntpath.py Python-3.12.2/Lib/test/tes def test_expandvars(self): with os_helper.EnvironmentVarGuard() as env: -diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/test_posixpath.py ---- Python-3.12.2.orig/Lib/test/test_posixpath.py 2024-10-15 11:25:54.853232897 +0800 -+++ Python-3.12.2/Lib/test/test_posixpath.py 2025-06-30 17:11:47.309186922 +0800 -@@ -2,7 +2,9 @@ import os +diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py +index cc4fd2f4c9512c..7ae54d29b5e434 100644 +--- a/Lib/test/test_posixpath.py ++++ b/Lib/test/test_posixpath.py +@@ -3,7 +3,9 @@ import posixpath import sys import unittest @@ -953,7 +995,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ from test import test_genericpath from test.support import import_helper from test.support import os_helper -@@ -36,6 +38,26 @@ def safe_rmdir(dirname): +@@ -37,6 +39,26 @@ def safe_rmdir(dirname): except OSError: pass @@ -980,7 +1022,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ class PosixPathTest(unittest.TestCase): def setUp(self): -@@ -398,32 +420,35 @@ class PosixPathTest(unittest.TestCase): +@@ -425,32 +447,35 @@ def test_normpath(self): self.assertEqual(result, expected) @skip_if_ABSTFN_contains_backslash @@ -1032,7 +1074,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ finally: os_helper.unlink(ABSTFN) -@@ -439,17 +464,124 @@ class PosixPathTest(unittest.TestCase): +@@ -466,15 +491,122 @@ def test_realpath_strict(self): finally: os_helper.unlink(ABSTFN) @@ -1143,8 +1185,8 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ finally: os_helper.unlink(ABSTFN) - @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash ++ @os_helper.skip_unless_symlink ++ @skip_if_ABSTFN_contains_backslash + @_parameterize({}, {'strict': ALLOW_MISSING}) + def test_realpath_missing_pardir(self, kwargs): + try: @@ -1154,12 +1196,10 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ + finally: + os_helper.unlink(os_helper.TESTFN) + -+ @os_helper.skip_unless_symlink -+ @skip_if_ABSTFN_contains_backslash + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash def test_realpath_symlink_loops(self): - # Bug #930024, return the path unchanged if we get into an infinite - # symlink loop in non-strict mode (default). -@@ -491,37 +623,38 @@ class PosixPathTest(unittest.TestCase): +@@ -518,37 +650,38 @@ def test_realpath_symlink_loops(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash @@ -1211,7 +1251,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ finally: os_helper.unlink(ABSTFN) os_helper.unlink(ABSTFN+"1") -@@ -532,13 +665,14 @@ class PosixPathTest(unittest.TestCase): +@@ -559,13 +692,14 @@ def test_realpath_symlink_loops_strict(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash @@ -1228,7 +1268,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ finally: os_helper.unlink(ABSTFN + '/self') os_helper.unlink(ABSTFN + '/link') -@@ -546,14 +680,15 @@ class PosixPathTest(unittest.TestCase): +@@ -573,14 +707,15 @@ def test_realpath_repeated_indirect_symlinks(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash @@ -1246,7 +1286,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ # Test using relative path as well. with os_helper.change_cwd(ABSTFN): -@@ -565,7 +700,8 @@ class PosixPathTest(unittest.TestCase): +@@ -592,7 +727,8 @@ def test_realpath_deep_recursion(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash @@ -1256,7 +1296,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ # We also need to resolve any symlinks in the parents of a relative # path passed to realpath. E.g.: current working directory is # /usr/doc with 'doc' being a symlink to /usr/share/doc. We call -@@ -576,7 +712,8 @@ class PosixPathTest(unittest.TestCase): +@@ -603,7 +739,8 @@ def test_realpath_resolve_parents(self): os.symlink(ABSTFN + "/y", ABSTFN + "/k") with os_helper.change_cwd(ABSTFN + "/k"): @@ -1266,7 +1306,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ finally: os_helper.unlink(ABSTFN + "/k") safe_rmdir(ABSTFN + "/y") -@@ -584,7 +721,8 @@ class PosixPathTest(unittest.TestCase): +@@ -611,7 +748,8 @@ def test_realpath_resolve_parents(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash @@ -1276,7 +1316,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ # Bug #990669: Symbolic links should be resolved before we # normalize the path. E.g.: if we have directories 'a', 'k' and 'y' # in the following hierarchy: -@@ -599,10 +737,10 @@ class PosixPathTest(unittest.TestCase): +@@ -626,10 +764,10 @@ def test_realpath_resolve_before_normalizing(self): os.symlink(ABSTFN + "/k/y", ABSTFN + "/link-y") # Absolute path. @@ -1289,7 +1329,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ ABSTFN + "/k") finally: os_helper.unlink(ABSTFN + "/link-y") -@@ -612,7 +750,8 @@ class PosixPathTest(unittest.TestCase): +@@ -639,7 +777,8 @@ def test_realpath_resolve_before_normalizing(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash @@ -1299,7 +1339,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ # Bug #1213894: The first component of the path, if not absolute, # must be resolved too. -@@ -622,13 +761,70 @@ class PosixPathTest(unittest.TestCase): +@@ -649,13 +788,70 @@ def test_realpath_resolve_first(self): os.symlink(ABSTFN, ABSTFN + "link") with os_helper.change_cwd(dirname(ABSTFN)): base = basename(ABSTFN) @@ -1372,7 +1412,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: -@@ -808,9 +1004,12 @@ class PathLikeTests(unittest.TestCase): +@@ -835,9 +1031,12 @@ def test_path_normpath(self): def test_path_abspath(self): self.assertPathEqual(self.path.abspath) @@ -1386,10 +1426,11 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_posixpath.py Python-3.12.2/Lib/test/ def test_path_relpath(self): self.assertPathEqual(self.path.relpath) -diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/test_tarfile.py ---- Python-3.12.2.orig/Lib/test/test_tarfile.py 2024-10-15 11:25:54.859232932 +0800 -+++ Python-3.12.2/Lib/test/test_tarfile.py 2025-06-30 17:11:47.310186928 +0800 -@@ -2539,7 +2539,8 @@ class MiscTest(unittest.TestCase): +diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py +index ae55bebbc80179..a184ba75a8851b 100644 +--- a/Lib/test/test_tarfile.py ++++ b/Lib/test/test_tarfile.py +@@ -2630,7 +2630,8 @@ def test__all__(self): 'PAX_NUMBER_FIELDS', 'stn', 'nts', 'nti', 'itn', 'calc_chksums', 'copyfileobj', 'filemode', 'EmptyHeaderError', 'TruncatedHeaderError', 'EOFHeaderError', 'InvalidHeaderError', @@ -1399,7 +1440,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te support.check__all__(self, tarfile, not_exported=not_exported) def test_useful_error_message_when_modules_missing(self): -@@ -2554,6 +2555,31 @@ class MiscTest(unittest.TestCase): +@@ -2645,6 +2646,31 @@ def test_useful_error_message_when_modules_missing(self): str(excinfo.exception), ) @@ -1431,7 +1472,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te class CommandLineTest(unittest.TestCase): -@@ -3114,6 +3140,10 @@ class NoneInfoExtractTests(ReadTest): +@@ -3205,6 +3231,10 @@ def check_files_present(self, directory): got_paths = set( p.relative_to(directory) for p in pathlib.Path(directory).glob('**/*')) @@ -1442,7 +1483,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te self.assertEqual(self.control_paths, got_paths) @contextmanager -@@ -3340,12 +3370,28 @@ class ArchiveMaker: +@@ -3431,12 +3461,28 @@ def __exit__(self, *exc): self.bio = None def add(self, name, *, type=None, symlink_to=None, hardlink_to=None, @@ -1473,7 +1514,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te if mode: tarinfo.mode = _filemode_to_int(mode) if symlink_to is not None: -@@ -3359,7 +3405,7 @@ class ArchiveMaker: +@@ -3450,7 +3496,7 @@ def add(self, name, *, type=None, symlink_to=None, hardlink_to=None, if type is not None: tarinfo.type = type if tarinfo.isreg(): @@ -1482,7 +1523,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te else: fileobj = None self.tar_w.addfile(tarinfo, fileobj) -@@ -3393,7 +3439,7 @@ class TestExtractionFilters(unittest.Tes +@@ -3484,7 +3530,7 @@ class TestExtractionFilters(unittest.TestCase): destdir = outerdir / 'dest' @contextmanager @@ -1491,7 +1532,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te """Extracts `tar` to `self.destdir` and allows checking the result If an error occurs, it must be checked using `expect_exception` -@@ -3402,27 +3448,40 @@ class TestExtractionFilters(unittest.Tes +@@ -3493,27 +3539,40 @@ def check_context(self, tar, filter): except the destination directory itself and parent directories of other files. When checking directories, do so before their contents. @@ -1535,7 +1576,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te """Check a single file. See check_context.""" if self.raised_exception: raise self.raised_exception -@@ -3441,26 +3500,45 @@ class TestExtractionFilters(unittest.Tes +@@ -3532,26 +3591,45 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None, # The symlink might be the same (textually) as what we expect, # but some systems change the link to an equivalent path, so # we fall back to samefile(). @@ -1584,10 +1625,11 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te def test_benign_file(self): with ArchiveMaker() as arc: -@@ -3546,6 +3624,80 @@ class TestExtractionFilters(unittest.Tes +@@ -3636,6 +3714,80 @@ def test_parent_symlink(self): + with self.check_context(arc.open(), 'data'): self.expect_file('parent/evil') - @symlink_test ++ @symlink_test + @os_helper.skip_unless_symlink + def test_realpath_limit_attack(self): + # (CVE-2025-4517) @@ -1661,11 +1703,10 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te + else: + self.assertEqual(exc.errno, errno.ENAMETOOLONG) + -+ @symlink_test + @symlink_test def test_parent_symlink2(self): # Test interplaying symlinks - # Inspired by 'dirsymlink2b' in jwilk/traversal-archives -@@ -3767,8 +3919,8 @@ class TestExtractionFilters(unittest.Tes +@@ -3858,8 +4010,8 @@ def test_chains(self): arc.add('symlink2', symlink_to=os.path.join( 'linkdir', 'hardlink2')) arc.add('targetdir/target', size=3) @@ -1676,7 +1717,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te for filter in 'tar', 'data', 'fully_trusted': with self.check_context(arc.open(), filter): -@@ -3784,6 +3936,129 @@ class TestExtractionFilters(unittest.Tes +@@ -3875,6 +4027,129 @@ def test_chains(self): self.expect_file('linkdir/symlink', size=3) self.expect_file('symlink2', size=3) @@ -1806,7 +1847,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te def test_modes(self): # Test how file modes are extracted # (Note that the modes are ignored on platforms without working chmod) -@@ -3897,7 +4172,7 @@ class TestExtractionFilters(unittest.Tes +@@ -3988,7 +4263,7 @@ def test_tar_filter(self): # The 'tar' filter returns TarInfo objects with the same name/type. # (It can also fail for particularly "evil" input, but we don't have # that in the test archive.) @@ -1815,7 +1856,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te for tarinfo in tar.getmembers(): filtered = tarfile.tar_filter(tarinfo, '') self.assertIs(filtered.name, tarinfo.name) -@@ -3906,7 +4181,7 @@ class TestExtractionFilters(unittest.Tes +@@ -3997,7 +4272,7 @@ def test_tar_filter(self): def test_data_filter(self): # The 'data' filter either raises, or returns TarInfo with the same # name/type. @@ -1824,7 +1865,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te for tarinfo in tar.getmembers(): try: filtered = tarfile.data_filter(tarinfo, '') -@@ -4036,13 +4311,13 @@ class TestExtractionFilters(unittest.Tes +@@ -4127,13 +4402,13 @@ def valueerror_filter(tarinfo, path): # If errorlevel is 0, errors affected by errorlevel are ignored with self.check_context(arc.open(errorlevel=0), extracterror_filter): @@ -1841,7 +1882,7 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te with self.check_context(arc.open(errorlevel=0), tarerror_filter): self.expect_exception(tarfile.TarError) -@@ -4053,7 +4328,7 @@ class TestExtractionFilters(unittest.Tes +@@ -4144,7 +4419,7 @@ def valueerror_filter(tarinfo, path): # If 1, all fatal errors are raised with self.check_context(arc.open(errorlevel=1), extracterror_filter): @@ -1850,9 +1891,11 @@ diff -urpN Python-3.12.2.orig/Lib/test/test_tarfile.py Python-3.12.2/Lib/test/te with self.check_context(arc.open(errorlevel=1), filtererror_filter): self.expect_exception(tarfile.FilterError) -diff -urpN Python-3.12.2.orig/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst Python-3.12.2/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst ---- Python-3.12.2.orig/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst 1970-01-01 08:00:00.000000000 +0800 -+++ Python-3.12.2/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst 2025-06-30 17:11:47.310186928 +0800 +diff --git a/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst b/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst +new file mode 100644 +index 00000000000000..08a0087e203671 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst @@ -0,0 +1,6 @@ +Fixes multiple issues that allowed ``tarfile`` extraction filters +(``filter="data"`` and ``filter="tar"``) to be bypassed using crafted diff --git a/python3.12.spec b/python3.12.spec index 3b6455617df23c2ff2e425092cd6885a4b45c7ea..205fc30a42777c6fdd560cb31a52c29f13b93094 100644 --- a/python3.12.spec +++ b/python3.12.spec @@ -360,8 +360,6 @@ This package contains /usr/bin/python - the "python" command that runs Python 3. %prep %autosetup -p1 -n Python-%{version} -exit 1 - cp %{SOURCE2} Tools/scripts %if %{with rpmwheels}