diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index b33dbe21b1fa19..829d64d0c02e89 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -650,6 +650,10 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. registered for that extension. In case none is found, a :exc:`ValueError` is raised. + Note that with *zip* format, absolute paths and paths containing a ``..`` + component, are not extracted. If you need such paths extracted, consider + using :func:`ZipFile.extractall`. + .. audit-event:: shutil.unpack_archive filename,extract_dir,format shutil.unpack_archive .. warning:: diff --git a/Lib/shutil.py b/Lib/shutil.py index ac1dd530528c0a..425156a370c835 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -10,6 +10,7 @@ import fnmatch import collections import errno +import logging try: import zlib @@ -1212,12 +1213,14 @@ def _unpack_zipfile(filename, extract_dir): raise ReadError("%s is not a zip file" % filename) zip = zipfile.ZipFile(filename) + skipped = 0 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: + skipped += 1 continue targetpath = os.path.join(extract_dir, *name.split('/')) @@ -1231,6 +1234,11 @@ def _unpack_zipfile(filename, extract_dir): open(targetpath, 'wb') as target: copyfileobj(source, target) finally: + if skipped: + import logging + logging.getLogger(__file__) + logging.warning(f'unpack {filename}: {skipped} file(s) skipped' + ' (due to absolute path or `..` path component)') zip.close() def _unpack_tarfile(filename, extract_dir): diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 6789fe4cc72e3a..e2a41496bf6ef6 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1686,6 +1686,14 @@ def check_unpack_archive_with_converter(self, format, converter): self.assertRaises(shutil.ReadError, unpack_archive, converter(TESTFN)) self.assertRaises(ValueError, unpack_archive, converter(TESTFN), format='xxx') + def test_unpack_archive_zip_warn_skipped(self): + tmpdir2 = self.mkdtemp() + with self.assertLogs(level='WARNING') as cm: + fn = support.findfile("testzip.zip") + unpack_archive(pathlib.Path(fn), pathlib.Path(tmpdir2)) + self.assertIn('1 file(s) skipped', cm.output[0]) + self.assertEqual(rlistdir(tmpdir2), ['test']) + def test_unpack_archive_tar(self): self.check_unpack_archive('tar') diff --git a/Lib/test/testzip.zip b/Lib/test/testzip.zip new file mode 100644 index 00000000000000..f25050560af1b6 Binary files /dev/null and b/Lib/test/testzip.zip differ diff --git a/Misc/NEWS.d/next/Library/2021-12-03-19-05-39.bpo-20907.f1_XQ1.rst b/Misc/NEWS.d/next/Library/2021-12-03-19-05-39.bpo-20907.f1_XQ1.rst new file mode 100644 index 00000000000000..941cbbad90c289 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-12-03-19-05-39.bpo-20907.f1_XQ1.rst @@ -0,0 +1,2 @@ +Added warning for skipped files in :func:`shutil.unpack_archive` using *zip* +format.