Fixes Issue #6972: The zipfile module no longer overwrites files outside of
its destination path when extracting malicious zip files.
diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst
index e975baa..df24bb7 100644
--- a/Doc/library/zipfile.rst
+++ b/Doc/library/zipfile.rst
@@ -242,6 +242,16 @@
.. versionadded:: 2.6
+ .. note::
+
+ If a member filename is an absolute path, a drive/UNC sharepoint and
+ leading (back)slashes will be stripped, e.g.: ``///foo/bar`` becomes
+ ``foo/bar`` on Unix, and ``ะก:\foo\bar`` becomes ``foo\bar`` on Windows.
+ And all ``".."`` components in a member filename will be removed, e.g.:
+ ``../../foo../../ba..r`` becomes ``foo../ba..r``. On Windows illegal
+ characters (``:``, ``<``, ``>``, ``|``, ``"``, ``?``, and ``*``)
+ replaced by underscore (``_``).
+
.. method:: ZipFile.read(name[, pwd])
diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
index a5ff90a..58d3c48 100644
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -26,7 +26,7 @@
SMALL_TEST_DATA = [('_ziptest1', '1q2w3e4r5t'),
('ziptest2dir/_ziptest2', 'qawsedrftg'),
- ('/ziptest2dir/ziptest3dir/_ziptest3', 'azsxdcfvgb'),
+ ('ziptest2dir/ziptest3dir/_ziptest3', 'azsxdcfvgb'),
('ziptest2dir/ziptest3dir/ziptest4dir/_ziptest3', '6y7u8i9o0p')]
@@ -391,10 +391,7 @@
writtenfile = zipfp.extract(fpath)
# make sure it was written to the right place
- if os.path.isabs(fpath):
- correctfile = os.path.join(os.getcwd(), fpath[1:])
- else:
- correctfile = os.path.join(os.getcwd(), fpath)
+ correctfile = os.path.join(os.getcwd(), fpath)
correctfile = os.path.normpath(correctfile)
self.assertEqual(writtenfile, correctfile)
@@ -414,10 +411,7 @@
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
zipfp.extractall()
for fpath, fdata in SMALL_TEST_DATA:
- if os.path.isabs(fpath):
- outfile = os.path.join(os.getcwd(), fpath[1:])
- else:
- outfile = os.path.join(os.getcwd(), fpath)
+ outfile = os.path.join(os.getcwd(), fpath)
self.assertEqual(fdata, open(outfile, "rb").read())
os.remove(outfile)
@@ -425,6 +419,80 @@
# remove the test file subdirectories
shutil.rmtree(os.path.join(os.getcwd(), 'ziptest2dir'))
+ def check_file(self, filename, content):
+ self.assertTrue(os.path.isfile(filename))
+ with open(filename, 'rb') as f:
+ self.assertEqual(f.read(), content)
+
+ def test_extract_hackers_arcnames(self):
+ hacknames = [
+ ('../foo/bar', 'foo/bar'),
+ ('foo/../bar', 'foo/bar'),
+ ('foo/../../bar', 'foo/bar'),
+ ('foo/bar/..', 'foo/bar'),
+ ('./../foo/bar', 'foo/bar'),
+ ('/foo/bar', 'foo/bar'),
+ ('/foo/../bar', 'foo/bar'),
+ ('/foo/../../bar', 'foo/bar'),
+ ('//foo/bar', 'foo/bar'),
+ ('../../foo../../ba..r', 'foo../ba..r'),
+ ]
+ if os.path.sep == '\\':
+ hacknames.extend([
+ (r'..\foo\bar', 'foo/bar'),
+ (r'..\/foo\/bar', 'foo/bar'),
+ (r'foo/\..\/bar', 'foo/bar'),
+ (r'foo\/../\bar', 'foo/bar'),
+ (r'C:foo/bar', 'foo/bar'),
+ (r'C:/foo/bar', 'foo/bar'),
+ (r'C://foo/bar', 'foo/bar'),
+ (r'C:\foo\bar', 'foo/bar'),
+ (r'//conky/mountpoint/foo/bar', 'foo/bar'),
+ (r'\\conky\mountpoint\foo\bar', 'foo/bar'),
+ (r'///conky/mountpoint/foo/bar', 'conky/mountpoint/foo/bar'),
+ (r'\\\conky\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'),
+ (r'//conky//mountpoint/foo/bar', 'conky/mountpoint/foo/bar'),
+ (r'\\conky\\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'),
+ (r'//?/C:/foo/bar', 'foo/bar'),
+ (r'\\?\C:\foo\bar', 'foo/bar'),
+ (r'C:/../C:/foo/bar', 'C_/foo/bar'),
+ (r'a:b\c<d>e|f"g?h*i', 'b/c_d_e_f_g_h_i'),
+ ])
+
+ for arcname, fixedname in hacknames:
+ content = b'foobar' + arcname.encode()
+ with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zipfp:
+ zipfp.writestr(arcname, content)
+
+ targetpath = os.path.join('target', 'subdir', 'subsub')
+ correctfile = os.path.join(targetpath, *fixedname.split('/'))
+
+ with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
+ writtenfile = zipfp.extract(arcname, targetpath)
+ self.assertEqual(writtenfile, correctfile)
+ self.check_file(correctfile, content)
+ shutil.rmtree('target')
+
+ with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
+ zipfp.extractall(targetpath)
+ self.check_file(correctfile, content)
+ shutil.rmtree('target')
+
+ correctfile = os.path.join(os.getcwd(), *fixedname.split('/'))
+
+ with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
+ writtenfile = zipfp.extract(arcname)
+ self.assertEqual(writtenfile, correctfile)
+ self.check_file(correctfile, content)
+ shutil.rmtree(fixedname.split('/')[0])
+
+ with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
+ zipfp.extractall()
+ self.check_file(correctfile, content)
+ shutil.rmtree(fixedname.split('/')[0])
+
+ os.remove(TESTFN2)
+
def test_writestr_compression(self):
zipfp = zipfile.ZipFile(TESTFN2, "w")
zipfp.writestr("a.txt", "hello world", compress_type=zipfile.ZIP_STORED)
diff --git a/Lib/zipfile.py b/Lib/zipfile.py
index 9dc4f57..31000ac 100644
--- a/Lib/zipfile.py
+++ b/Lib/zipfile.py
@@ -1040,17 +1040,22 @@
"""
# build the destination pathname, replacing
# forward slashes to platform specific separators.
- # Strip trailing path separator, unless it represents the root.
- if (targetpath[-1:] in (os.path.sep, os.path.altsep)
- and len(os.path.splitdrive(targetpath)[1]) > 1):
- targetpath = targetpath[:-1]
+ arcname = member.filename.replace('/', os.path.sep)
- # don't include leading "/" from file name if present
- if member.filename[0] == '/':
- targetpath = os.path.join(targetpath, member.filename[1:])
- else:
- targetpath = os.path.join(targetpath, member.filename)
+ if os.path.altsep:
+ arcname = arcname.replace(os.path.altsep, os.path.sep)
+ # interpret absolute pathname as relative, remove drive letter or
+ # UNC path, redundant separators, "." and ".." components.
+ arcname = os.path.splitdrive(arcname)[1]
+ arcname = os.path.sep.join(x for x in arcname.split(os.path.sep)
+ if x not in ('', os.path.curdir, os.path.pardir))
+ # filter illegal characters on Windows
+ if os.path.sep == '\\':
+ illegal = ':<>|"?*'
+ table = str.maketrans(illegal, '_' * len(illegal))
+ arcname = arcname.translate(table)
+ targetpath = os.path.join(targetpath, arcname)
targetpath = os.path.normpath(targetpath)
# Create all upper directories if necessary.
diff --git a/Misc/NEWS b/Misc/NEWS
index c28ff9c..2d46d7d 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -202,6 +202,9 @@
Library
-------
+- Issue #6972: The zipfile module no longer overwrites files outside of
+ its destination path when extracting malicious zip files.
+
- Issue #17049: Localized calendar methods now return unicode if a locale
includes an encoding and the result string contains month or weekday (was
regression from Python 2.6).