Skip to content

Commit 7c5ad11

Browse files
Fix vulnerability in shutil.unpack_archive() in ZIP files
Use ZipFile.extractall() to sanitize file names and extract files. Files with invalid names (e.g. absolute paths) are now extracted with different names instead of been skipped or written out of the destination directory. Files containing ".." in the name are no longer skipped.
1 parent 0e4b1ba commit 7c5ad11

File tree

2 files changed

+54
-23
lines changed

2 files changed

+54
-23
lines changed

Lib/shutil.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,27 +1314,8 @@ def _unpack_zipfile(filename, extract_dir):
13141314
if not zipfile.is_zipfile(filename):
13151315
raise ReadError("%s is not a zip file" % filename)
13161316

1317-
zip = zipfile.ZipFile(filename)
1318-
try:
1319-
for info in zip.infolist():
1320-
name = info.filename
1321-
1322-
# don't extract absolute paths or ones with .. in them
1323-
if name.startswith('/') or '..' in name:
1324-
continue
1325-
1326-
targetpath = os.path.join(extract_dir, *name.split('/'))
1327-
if not targetpath:
1328-
continue
1329-
1330-
_ensure_directory(targetpath)
1331-
if not name.endswith('/'):
1332-
# file
1333-
with zip.open(name, 'r') as source, \
1334-
open(targetpath, 'wb') as target:
1335-
copyfileobj(source, target)
1336-
finally:
1337-
zip.close()
1317+
with zipfile.ZipFile(filename) as zip:
1318+
zip.extractall(extract_dir)
13381319

13391320
def _unpack_tarfile(filename, extract_dir, *, filter=None):
13401321
"""Unpack tar/tar.gz/tar.bz2/tar.xz/tar.zst `filename` to `extract_dir`

Lib/test/test_shutil.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,8 +2110,6 @@ def test_make_zipfile_rootdir_nodir(self):
21102110
def check_unpack_archive(self, format, **kwargs):
21112111
self.check_unpack_archive_with_converter(
21122112
format, lambda path: path, **kwargs)
2113-
self.check_unpack_archive_with_converter(
2114-
format, FakePath, **kwargs)
21152113
self.check_unpack_archive_with_converter(format, FakePath, **kwargs)
21162114

21172115
def check_unpack_archive_with_converter(self, format, converter, **kwargs):
@@ -2168,6 +2166,58 @@ def test_unpack_archive_zip(self):
21682166
with self.assertRaises(TypeError):
21692167
self.check_unpack_archive('zip', filter='data')
21702168

2169+
def test_unpack_archive_zip_badpaths(self):
2170+
srcdir = self.mkdtemp()
2171+
zipname = os.path.join(srcdir, 'test.zip')
2172+
abspath = os.path.join(srcdir, 'abspath')
2173+
with zipfile.ZipFile(zipname, 'w') as zf:
2174+
zf.writestr(abspath, 'badfile')
2175+
zf.writestr(os.sep + abspath, 'badfile')
2176+
zf.writestr('/abspath2', 'badfile')
2177+
if os.name == 'nt':
2178+
zf.writestr('C:/abspath3', 'badfile')
2179+
zf.writestr('C:\\abspath4', 'badfile')
2180+
zf.writestr('C:abspath5', 'badfile')
2181+
zf.writestr('C:/C:/abspath6', 'badfile')
2182+
zf.writestr('../relpath', 'badfile')
2183+
zf.writestr(os.pardir + os.sep + 'relpath2', 'badfile')
2184+
zf.writestr('good/file', 'goodfile')
2185+
zf.writestr('good..file', 'goodfile')
2186+
2187+
dstdir = os.path.join(self.mkdtemp(), 'dst')
2188+
unpack_archive(zipname, dstdir)
2189+
self.assertTrue(os.path.isfile(os.path.join(dstdir, 'good', 'file')))
2190+
self.assertTrue(os.path.isfile(os.path.join(dstdir, 'good..file')))
2191+
self.assertFalse(os.path.exists(abspath))
2192+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'abspath2')))
2193+
if os.name == 'nt':
2194+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'abspath3')))
2195+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'abspath4')))
2196+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'abspath5')))
2197+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'C_', 'abspath6')))
2198+
self.assertFalse(os.path.exists(os.path.join(dstdir, '..', 'relpath')))
2199+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'relpath')))
2200+
self.assertFalse(os.path.exists(os.path.join(dstdir, os.pardir, 'relpath2')))
2201+
self.assertTrue(os.path.exists(os.path.join(dstdir, 'relpath2')))
2202+
2203+
dstdir2 = os.path.join(self.mkdtemp(), 'dst')
2204+
os.mkdir(dstdir2)
2205+
with os_helper.change_cwd(dstdir2):
2206+
unpack_archive(zipname, '')
2207+
self.assertTrue(os.path.isfile(os.path.join('good', 'file')))
2208+
self.assertTrue(os.path.isfile('good..file'))
2209+
self.assertFalse(os.path.exists(abspath))
2210+
self.assertTrue(os.path.exists('abspath2'))
2211+
if os.name == 'nt':
2212+
self.assertTrue(os.path.exists('abspath3'))
2213+
self.assertTrue(os.path.exists('abspath4'))
2214+
self.assertTrue(os.path.exists('abspath5'))
2215+
self.assertTrue(os.path.exists(os.path.join('c_', 'abspath6')))
2216+
self.assertFalse(os.path.exists(os.path.join('..', 'relpath')))
2217+
self.assertTrue(os.path.exists('relpath'))
2218+
self.assertFalse(os.path.exists(os.path.join(os.pardir, 'relpath2')))
2219+
self.assertTrue(os.path.exists('relpath2'))
2220+
21712221
def test_unpack_registry(self):
21722222

21732223
formats = get_unpack_formats()

0 commit comments

Comments
 (0)