diff --git a/Lib/shutil.py b/Lib/shutil.py index 44ccdbb503d4fb..42661dec3f60c5 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1313,31 +1313,10 @@ def _unpack_zipfile(filename, extract_dir): """Unpack zip `filename` to `extract_dir` """ import zipfile # late import for breaking circular dependency - if not zipfile.is_zipfile(filename): raise ReadError("%s is not a zip file" % filename) - - zip = zipfile.ZipFile(filename) - try: - for info in zip.infolist(): - name = info.filename - - # don't extract absolute paths or ones with .. in them - if name.startswith('/') or '..' in name: - continue - - targetpath = os.path.join(extract_dir, *name.split('/')) - if not targetpath: - continue - - _ensure_directory(targetpath) - if not name.endswith('/'): - # file - with zip.open(name, 'r') as source, \ - open(targetpath, 'wb') as target: - copyfileobj(source, target) - finally: - zip.close() + with zipfile.ZipFile(filename) as zf: + zf.extractall(extract_dir) def _unpack_tarfile(filename, extract_dir, *, filter=None): """Unpack tar/tar.gz/tar.bz2/tar.xz/tar.zst `filename` to `extract_dir` diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index a4bd113bc7f1fc..40ef6258913648 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -3519,7 +3519,27 @@ def test_module_all_attribute(self): self.assertEqual(set(shutil.__all__), set(target_api)) with self.assertWarns(DeprecationWarning): from shutil import ExecError # noqa: F401 - - if __name__ == '__main__': unittest.main() +class TestShutilZipTraversal(unittest.TestCase): + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.extract_dir = os.path.join(self.tmp_dir, "extract") + os.mkdir(self.extract_dir) + def tearDown(self): + shutil.rmtree(self.tmp_dir) + @unittest.skipUnless(sys.platform == 'win32', 'Windows-specific traversal test') + @support.requires_zlib() + def test_unpack_zipfile_traversal_windows_drive(self): + # Create a ZIP file with a drive prefixed path + zip_path = os.path.join(self.tmp_dir, "test.zip") + with zipfile.ZipFile(zip_path, 'w') as zf: + zf.writestr("D:/traversal.txt", "found you") + shutil.unpack_archive(zip_path, self.extract_dir) + self.assertFalse(os.path.exists("D:/traversal.txt")) + found = False + for root, dirs, files in os.walk(self.extract_dir): + if "traversal.txt" in files: + found = True + break + self.assertTrue(found, "Extracted file not found within extract_dir") diff --git a/Misc/NEWS.d/next/Security/2026-03-29-08-14-09.gh-issue-146581.81t9a4.rst b/Misc/NEWS.d/next/Security/2026-03-29-08-14-09.gh-issue-146581.81t9a4.rst new file mode 100644 index 00000000000000..e31a8b943f1620 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-03-29-08-14-09.gh-issue-146581.81t9a4.rst @@ -0,0 +1 @@ +Fix a directory traversal vulnerability in shutil.unpack_archive for ZIP files on Windows by refactoring _unpack_zipfile to use zipfile.ZipFile.extractall. This leverages the built in, hardened path sanitization in the zipfile module to safely handle drive prefixed paths. Patch by Shrey Naithani.