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

its destination path when extracting malicious zip files.
This commit is contained in:
Gregory P. Smith 2013-02-01 11:40:18 -08:00
parent f39d52f8cb
commit 608cc451c7
4 changed files with 104 additions and 18 deletions

View File

@ -242,6 +242,16 @@ ZipFile Objects
.. versionadded:: 2.6 .. 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]) .. method:: ZipFile.read(name[, pwd])

View File

@ -26,7 +26,7 @@ FIXEDTEST_SIZE = 1000
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')]
@ -391,10 +391,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)
@ -414,10 +411,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)
self.assertEqual(fdata, open(outfile, "rb").read()) self.assertEqual(fdata, open(outfile, "rb").read())
os.remove(outfile) os.remove(outfile)
@ -425,6 +419,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 == '\\':
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)

View File

@ -1040,17 +1040,22 @@ class ZipFile(object):
""" """
# 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):
targetpath = targetpath[:-1]
# don't include leading "/" from file name if present if os.path.altsep:
if member.filename[0] == '/': arcname = arcname.replace(os.path.altsep, os.path.sep)
targetpath = os.path.join(targetpath, member.filename[1:]) # interpret absolute pathname as relative, remove drive letter or
else: # UNC path, redundant separators, "." and ".." components.
targetpath = os.path.join(targetpath, member.filename) 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) targetpath = os.path.normpath(targetpath)
# Create all upper directories if necessary. # Create all upper directories if necessary.

View File

@ -202,6 +202,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 #17049: Localized calendar methods now return unicode if a locale - Issue #17049: Localized calendar methods now return unicode if a locale
includes an encoding and the result string contains month or weekday (was includes an encoding and the result string contains month or weekday (was
regression from Python 2.6). regression from Python 2.6).