Kaydet (Commit) 608cc451 authored tarafından 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.
üst f39d52f8
...@@ -242,6 +242,16 @@ ZipFile Objects ...@@ -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])
......
...@@ -26,7 +26,7 @@ FIXEDTEST_SIZE = 1000 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -1040,17 +1040,22 @@ class ZipFile(object): ...@@ -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): 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.
......
...@@ -202,6 +202,9 @@ Core and Builtins ...@@ -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).
......
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