Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b47acbf

Browse files
committed
Fixes Issue #6972: The zipfile module no longer overwrites files outside of
its destination path when extracting malicious zip files.
1 parent 04d86c7 commit b47acbf

4 files changed

Lines changed: 108 additions & 25 deletions

File tree

Doc/library/zipfile.rst

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ ZipFile Objects
214214
to extract to. *member* can be a filename or a :class:`ZipInfo` object.
215215
*pwd* is the password used for encrypted files.
216216

217+
.. note::
218+
219+
If a member filename is an absolute path, a drive/UNC sharepoint and
220+
leading (back)slashes will be stripped, e.g.: ``///foo/bar`` becomes
221+
``foo/bar`` on Unix, and ``С:\foo\bar`` becomes ``foo\bar`` on Windows.
222+
And all ``".."`` components in a member filename will be removed, e.g.:
223+
``../../foo../../ba..r`` becomes ``foo../ba..r``. On Windows illegal
224+
characters (``:``, ``<``, ``>``, ``|``, ``"``, ``?``, and ``*``)
225+
replaced by underscore (``_``).
226+
217227

218228
.. method:: ZipFile.extractall(path=None, members=None, pwd=None)
219229

@@ -222,12 +232,9 @@ ZipFile Objects
222232
be a subset of the list returned by :meth:`namelist`. *pwd* is the password
223233
used for encrypted files.
224234

225-
.. warning::
235+
.. note::
226236

227-
Never extract archives from untrusted sources without prior inspection.
228-
It is possible that files are created outside of *path*, e.g. members
229-
that have absolute filenames starting with ``"/"`` or filenames with two
230-
dots ``".."``.
237+
See :meth:`extract` note.
231238

232239

233240
.. method:: ZipFile.printdir()

Lib/test/test_zipfile.py

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
SMALL_TEST_DATA = [('_ziptest1', '1q2w3e4r5t'),
3131
('ziptest2dir/_ziptest2', 'qawsedrftg'),
32-
('/ziptest2dir/ziptest3dir/_ziptest3', 'azsxdcfvgb'),
32+
('ziptest2dir/ziptest3dir/_ziptest3', 'azsxdcfvgb'),
3333
('ziptest2dir/ziptest3dir/ziptest4dir/_ziptest3', '6y7u8i9o0p')]
3434

3535

@@ -409,10 +409,7 @@ def test_extract(self):
409409
writtenfile = zipfp.extract(fpath)
410410

411411
# make sure it was written to the right place
412-
if os.path.isabs(fpath):
413-
correctfile = os.path.join(os.getcwd(), fpath[1:])
414-
else:
415-
correctfile = os.path.join(os.getcwd(), fpath)
412+
correctfile = os.path.join(os.getcwd(), fpath)
416413
correctfile = os.path.normpath(correctfile)
417414

418415
self.assertEqual(writtenfile, correctfile)
@@ -434,10 +431,7 @@ def test_extract_all(self):
434431
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
435432
zipfp.extractall()
436433
for fpath, fdata in SMALL_TEST_DATA:
437-
if os.path.isabs(fpath):
438-
outfile = os.path.join(os.getcwd(), fpath[1:])
439-
else:
440-
outfile = os.path.join(os.getcwd(), fpath)
434+
outfile = os.path.join(os.getcwd(), fpath)
441435

442436
with open(outfile, "rb") as f:
443437
self.assertEqual(fdata.encode(), f.read())
@@ -447,6 +441,80 @@ def test_extract_all(self):
447441
# remove the test file subdirectories
448442
shutil.rmtree(os.path.join(os.getcwd(), 'ziptest2dir'))
449443

444+
def check_file(self, filename, content):
445+
self.assertTrue(os.path.isfile(filename))
446+
with open(filename, 'rb') as f:
447+
self.assertEqual(f.read(), content)
448+
449+
def test_extract_hackers_arcnames(self):
450+
hacknames = [
451+
('../foo/bar', 'foo/bar'),
452+
('foo/../bar', 'foo/bar'),
453+
('foo/../../bar', 'foo/bar'),
454+
('foo/bar/..', 'foo/bar'),
455+
('./../foo/bar', 'foo/bar'),
456+
('/foo/bar', 'foo/bar'),
457+
('/foo/../bar', 'foo/bar'),
458+
('/foo/../../bar', 'foo/bar'),
459+
('//foo/bar', 'foo/bar'),
460+
('../../foo../../ba..r', 'foo../ba..r'),
461+
]
462+
if os.path.sep == '\\': # Windows.
463+
hacknames.extend([
464+
(r'..\foo\bar', 'foo/bar'),
465+
(r'..\/foo\/bar', 'foo/bar'),
466+
(r'foo/\..\/bar', 'foo/bar'),
467+
(r'foo\/../\bar', 'foo/bar'),
468+
(r'C:foo/bar', 'foo/bar'),
469+
(r'C:/foo/bar', 'foo/bar'),
470+
(r'C://foo/bar', 'foo/bar'),
471+
(r'C:\foo\bar', 'foo/bar'),
472+
(r'//conky/mountpoint/foo/bar', 'foo/bar'),
473+
(r'\\conky\mountpoint\foo\bar', 'foo/bar'),
474+
(r'///conky/mountpoint/foo/bar', 'conky/mountpoint/foo/bar'),
475+
(r'\\\conky\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'),
476+
(r'//conky//mountpoint/foo/bar', 'conky/mountpoint/foo/bar'),
477+
(r'\\conky\\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'),
478+
(r'//?/C:/foo/bar', 'foo/bar'),
479+
(r'\\?\C:\foo\bar', 'foo/bar'),
480+
(r'C:/../C:/foo/bar', 'C_/foo/bar'),
481+
(r'a:b\c<d>e|f"g?h*i', 'b/c_d_e_f_g_h_i'),
482+
])
483+
484+
for arcname, fixedname in hacknames:
485+
content = b'foobar' + arcname.encode()
486+
with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zipfp:
487+
zipfp.writestr(arcname, content)
488+
489+
targetpath = os.path.join('target', 'subdir', 'subsub')
490+
correctfile = os.path.join(targetpath, *fixedname.split('/'))
491+
492+
with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
493+
writtenfile = zipfp.extract(arcname, targetpath)
494+
self.assertEqual(writtenfile, correctfile)
495+
self.check_file(correctfile, content)
496+
shutil.rmtree('target')
497+
498+
with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
499+
zipfp.extractall(targetpath)
500+
self.check_file(correctfile, content)
501+
shutil.rmtree('target')
502+
503+
correctfile = os.path.join(os.getcwd(), *fixedname.split('/'))
504+
505+
with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
506+
writtenfile = zipfp.extract(arcname)
507+
self.assertEqual(writtenfile, correctfile)
508+
self.check_file(correctfile, content)
509+
shutil.rmtree(fixedname.split('/')[0])
510+
511+
with zipfile.ZipFile(TESTFN2, 'r') as zipfp:
512+
zipfp.extractall()
513+
self.check_file(correctfile, content)
514+
shutil.rmtree(fixedname.split('/')[0])
515+
516+
os.remove(TESTFN2)
517+
450518
def test_writestr_compression(self):
451519
zipfp = zipfile.ZipFile(TESTFN2, "w")
452520
zipfp.writestr("a.txt", "hello world", compress_type=zipfile.ZIP_STORED)

Lib/zipfile.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,17 +1062,22 @@ def _extract_member(self, member, targetpath, pwd):
10621062
"""
10631063
# build the destination pathname, replacing
10641064
# forward slashes to platform specific separators.
1065-
# Strip trailing path separator, unless it represents the root.
1066-
if (targetpath[-1:] in (os.path.sep, os.path.altsep)
1067-
and len(os.path.splitdrive(targetpath)[1]) > 1):
1068-
targetpath = targetpath[:-1]
1069-
1070-
# don't include leading "/" from file name if present
1071-
if member.filename[0] == '/':
1072-
targetpath = os.path.join(targetpath, member.filename[1:])
1073-
else:
1074-
targetpath = os.path.join(targetpath, member.filename)
1075-
1065+
arcname = member.filename.replace('/', os.path.sep)
1066+
1067+
if os.path.altsep:
1068+
arcname = arcname.replace(os.path.altsep, os.path.sep)
1069+
# interpret absolute pathname as relative, remove drive letter or
1070+
# UNC path, redundant separators, "." and ".." components.
1071+
arcname = os.path.splitdrive(arcname)[1]
1072+
arcname = os.path.sep.join(x for x in arcname.split(os.path.sep)
1073+
if x not in ('', os.path.curdir, os.path.pardir))
1074+
# filter illegal characters on Windows
1075+
if os.path.sep == '\\':
1076+
illegal = ':<>|"?*'
1077+
table = str.maketrans(illegal, '_' * len(illegal))
1078+
arcname = arcname.translate(table)
1079+
1080+
targetpath = os.path.join(targetpath, arcname)
10761081
targetpath = os.path.normpath(targetpath)
10771082

10781083
# Create all upper directories if necessary.

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ Core and Builtins
216216
Library
217217
-------
218218

219+
- Issue #6972: The zipfile module no longer overwrites files outside of
220+
its destination path when extracting malicious zip files.
221+
219222
- Issue #4844: ZipFile now raises BadZipFile when opens a ZIP file with an
220223
incomplete "End of Central Directory" record. Original patch by Guilherme
221224
Polo and Alan McIntyre.

0 commit comments

Comments
 (0)