Commit b47acbf4 authored by Gregory P. Smith's avatar Gregory P. Smith

Fixes Issue #6972: The zipfile module no longer overwrites files outside of

its destination path when extracting malicious zip files.
parent 04d86c7c
...@@ -214,6 +214,16 @@ ZipFile Objects ...@@ -214,6 +214,16 @@ ZipFile Objects
to extract to. *member* can be a filename or a :class:`ZipInfo` object. to extract to. *member* can be a filename or a :class:`ZipInfo` object.
*pwd* is the password used for encrypted files. *pwd* is the password used for encrypted files.
.. 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.extractall(path=None, members=None, pwd=None) .. method:: ZipFile.extractall(path=None, members=None, pwd=None)
...@@ -222,12 +232,9 @@ ZipFile Objects ...@@ -222,12 +232,9 @@ ZipFile Objects
be a subset of the list returned by :meth:`namelist`. *pwd* is the password be a subset of the list returned by :meth:`namelist`. *pwd* is the password
used for encrypted files. used for encrypted files.
.. warning:: .. note::
Never extract archives from untrusted sources without prior inspection. See :meth:`extract` note.
It is possible that files are created outside of *path*, e.g. members
that have absolute filenames starting with ``"/"`` or filenames with two
dots ``".."``.
.. method:: ZipFile.printdir() .. method:: ZipFile.printdir()
......
...@@ -29,7 +29,7 @@ DATAFILES_DIR = 'zipfile_datafiles' ...@@ -29,7 +29,7 @@ DATAFILES_DIR = 'zipfile_datafiles'
SMALL_TEST_DATA = [('_ziptest1', '1q2w3e4r5t'), SMALL_TEST_DATA = [('_ziptest1', '1q2w3e4r5t'),
('ziptest2dir/_ziptest2', 'qawsedrftg'), ('ziptest2dir/_ziptest2', 'qawsedrftg'),
('/ziptest2dir/ziptest3dir/_ziptest3', 'azsxdcfvgb'), ('ziptest2dir/ziptest3dir/_ziptest3', 'azsxdcfvgb'),
('ziptest2dir/ziptest3dir/ziptest4dir/_ziptest3', '6y7u8i9o0p')] ('ziptest2dir/ziptest3dir/ziptest4dir/_ziptest3', '6y7u8i9o0p')]
...@@ -409,10 +409,7 @@ class TestsWithSourceFile(unittest.TestCase): ...@@ -409,10 +409,7 @@ class TestsWithSourceFile(unittest.TestCase):
writtenfile = zipfp.extract(fpath) writtenfile = zipfp.extract(fpath)
# make sure it was written to the right place # make sure it was written to the right place
if os.path.isabs(fpath): correctfile = os.path.join(os.getcwd(), fpath)
correctfile = os.path.join(os.getcwd(), fpath[1:])
else:
correctfile = os.path.join(os.getcwd(), fpath)
correctfile = os.path.normpath(correctfile) correctfile = os.path.normpath(correctfile)
self.assertEqual(writtenfile, correctfile) self.assertEqual(writtenfile, correctfile)
...@@ -434,10 +431,7 @@ class TestsWithSourceFile(unittest.TestCase): ...@@ -434,10 +431,7 @@ class TestsWithSourceFile(unittest.TestCase):
with zipfile.ZipFile(TESTFN2, "r") as zipfp: with zipfile.ZipFile(TESTFN2, "r") as zipfp:
zipfp.extractall() zipfp.extractall()
for fpath, fdata in SMALL_TEST_DATA: for fpath, fdata in SMALL_TEST_DATA:
if os.path.isabs(fpath): outfile = os.path.join(os.getcwd(), fpath)
outfile = os.path.join(os.getcwd(), fpath[1:])
else:
outfile = os.path.join(os.getcwd(), fpath)
with open(outfile, "rb") as f: with open(outfile, "rb") as f:
self.assertEqual(fdata.encode(), f.read()) self.assertEqual(fdata.encode(), f.read())
...@@ -447,6 +441,80 @@ class TestsWithSourceFile(unittest.TestCase): ...@@ -447,6 +441,80 @@ class TestsWithSourceFile(unittest.TestCase):
# remove the test file subdirectories # remove the test file subdirectories
shutil.rmtree(os.path.join(os.getcwd(), 'ziptest2dir')) 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 == '\\': # Windows.
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): def test_writestr_compression(self):
zipfp = zipfile.ZipFile(TESTFN2, "w") zipfp = zipfile.ZipFile(TESTFN2, "w")
zipfp.writestr("a.txt", "hello world", compress_type=zipfile.ZIP_STORED) zipfp.writestr("a.txt", "hello world", compress_type=zipfile.ZIP_STORED)
......
...@@ -1062,17 +1062,22 @@ class ZipFile: ...@@ -1062,17 +1062,22 @@ class ZipFile:
""" """
# build the destination pathname, replacing # build the destination pathname, replacing
# forward slashes to platform specific separators. # forward slashes to platform specific separators.
# Strip trailing path separator, unless it represents the root. arcname = member.filename.replace('/', os.path.sep)
if (targetpath[-1:] in (os.path.sep, os.path.altsep)
and len(os.path.splitdrive(targetpath)[1]) > 1): if os.path.altsep:
targetpath = targetpath[:-1] arcname = arcname.replace(os.path.altsep, os.path.sep)
# interpret absolute pathname as relative, remove drive letter or
# don't include leading "/" from file name if present # UNC path, redundant separators, "." and ".." components.
if member.filename[0] == '/': arcname = os.path.splitdrive(arcname)[1]
targetpath = os.path.join(targetpath, member.filename[1:]) arcname = os.path.sep.join(x for x in arcname.split(os.path.sep)
else: if x not in ('', os.path.curdir, os.path.pardir))
targetpath = os.path.join(targetpath, member.filename) # 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) targetpath = os.path.normpath(targetpath)
# Create all upper directories if necessary. # Create all upper directories if necessary.
......
...@@ -216,6 +216,9 @@ Core and Builtins ...@@ -216,6 +216,9 @@ Core and Builtins
Library Library
------- -------
- Issue #6972: The zipfile module no longer overwrites files outside of
its destination path when extracting malicious zip files.
- Issue #4844: ZipFile now raises BadZipFile when opens a ZIP file with an - Issue #4844: ZipFile now raises BadZipFile when opens a ZIP file with an
incomplete "End of Central Directory" record. Original patch by Guilherme incomplete "End of Central Directory" record. Original patch by Guilherme
Polo and Alan McIntyre. Polo and Alan McIntyre.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment