Add some verification for "dangerous" tarfiles
This commit is contained in:
parent
a7ebb587e1
commit
9516990693
@ -15,7 +15,7 @@ from . import abstract, UnknownMemberPolicy, parser_factory
|
|||||||
assert Set
|
assert Set
|
||||||
assert Pattern
|
assert Pattern
|
||||||
|
|
||||||
# pylint: disable=not-callable,assignment-from-no-return
|
# pylint: disable=not-callable,assignment-from-no-return,too-many-branches
|
||||||
|
|
||||||
# An ArchiveClass is a class representing an archive,
|
# An ArchiveClass is a class representing an archive,
|
||||||
# while an ArchiveMember is a class representing an element
|
# while an ArchiveMember is a class representing an element
|
||||||
@ -238,6 +238,57 @@ class TarParser(ArchiveBasedAbstractParser):
|
|||||||
def is_archive_valid(self):
|
def is_archive_valid(self):
|
||||||
if tarfile.is_tarfile(self.filename) is False:
|
if tarfile.is_tarfile(self.filename) is False:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
self.__check_tarfile_safety()
|
||||||
|
|
||||||
|
def __check_tarfile_safety(self):
|
||||||
|
"""Checks if the tarfile doesn't have any "suspicious" members.
|
||||||
|
|
||||||
|
This is a rewrite of this patch: https://bugs.python.org/file47826/safetarfile-4.diff
|
||||||
|
inspired by this bug from 2014: https://bugs.python.org/issue21109
|
||||||
|
because Python's stdlib doesn't provide a way to "safely" extract
|
||||||
|
things from a tar file.
|
||||||
|
"""
|
||||||
|
names = set()
|
||||||
|
with tarfile.open(self.filename) as f:
|
||||||
|
members = f.getmembers()
|
||||||
|
for member in members:
|
||||||
|
name = member.name
|
||||||
|
if os.path.isabs(name):
|
||||||
|
raise ValueError("The archive %s contains a file with an " \
|
||||||
|
"absolute path: %s" % (self.filename, name))
|
||||||
|
elif os.path.normpath(name).startswith('../') or '/../' in name:
|
||||||
|
raise ValueError("The archive %s contains a file with an " \
|
||||||
|
"path traversal attack: %s" % (self.filename, name))
|
||||||
|
|
||||||
|
if name in names:
|
||||||
|
raise ValueError("The archive %s contains two times the same " \
|
||||||
|
"file: %s" % (self.filename, name))
|
||||||
|
else:
|
||||||
|
names.add(name)
|
||||||
|
|
||||||
|
if member.isfile():
|
||||||
|
if member.mode & stat.S_ISUID:
|
||||||
|
raise ValueError("The archive %s contains a setuid file: %s" % \
|
||||||
|
(self.filename, name))
|
||||||
|
elif member.mode & stat.S_ISGID:
|
||||||
|
raise ValueError("The archive %s contains a setgid file: %s" % \
|
||||||
|
(self.filename, name))
|
||||||
|
elif member.issym():
|
||||||
|
linkname = member.linkname
|
||||||
|
if os.path.normpath(linkname).startswith('..'):
|
||||||
|
raise ValueError('The archive %s contains a symlink pointing' \
|
||||||
|
'outside of the archive via a path traversal: %s -> %s' % \
|
||||||
|
(self.filename, name, linkname))
|
||||||
|
if os.path.isabs(linkname):
|
||||||
|
raise ValueError('The archive %s contains a symlink pointing' \
|
||||||
|
'outside of the archive: %s -> %s' % \
|
||||||
|
(self.filename, name, linkname))
|
||||||
|
elif member.isdev():
|
||||||
|
raise ValueError("The archive %s contains a non-regular " \
|
||||||
|
"file: %s" % (self.filename, name))
|
||||||
|
elif member.islnk():
|
||||||
|
raise ValueError("The archive %s contains a hardlink: %s" \
|
||||||
|
% (self.filename, name))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _clean_member(member: ArchiveMember) -> ArchiveMember:
|
def _clean_member(member: ArchiveMember) -> ArchiveMember:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
@ -325,6 +326,7 @@ class TestReadOnlyArchiveMembers(unittest.TestCase):
|
|||||||
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
||||||
tarinfo.mtime = time.time()
|
tarinfo.mtime = time.time()
|
||||||
tarinfo.uid = 1337
|
tarinfo.uid = 1337
|
||||||
|
tarinfo.gid = 0
|
||||||
tarinfo.mode = 0o000
|
tarinfo.mode = 0o000
|
||||||
tarinfo.size = os.stat('./tests/data/dirty.jpg').st_size
|
tarinfo.size = os.stat('./tests/data/dirty.jpg').st_size
|
||||||
with open('./tests/data/dirty.jpg', 'rb') as f:
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
@ -340,3 +342,121 @@ class TestReadOnlyArchiveMembers(unittest.TestCase):
|
|||||||
os.remove('./tests/data/clean.tar')
|
os.remove('./tests/data/clean.tar')
|
||||||
os.remove('./tests/data/clean.cleaned.tar')
|
os.remove('./tests/data/clean.cleaned.tar')
|
||||||
|
|
||||||
|
|
||||||
|
class TestPathTraversalArchiveMembers(unittest.TestCase):
|
||||||
|
def test_tar_traversal(self):
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/dirty.png')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
||||||
|
tarinfo.name = '../../../../../../../../../../tmp/mat2_test.png'
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
|
||||||
|
def test_tar_absolute_path(self):
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/dirty.png')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
||||||
|
tarinfo.name = '/etc/passwd'
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
|
||||||
|
def test_tar_duplicate_file(self):
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
for _ in range(3):
|
||||||
|
zout.add('./tests/data/dirty.png')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
|
||||||
|
def test_tar_setuid(self):
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/dirty.png')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
||||||
|
tarinfo.mode |= stat.S_ISUID
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
|
||||||
|
def test_tar_setgid(self):
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/dirty.png')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/dirty.jpg')
|
||||||
|
tarinfo.mode |= stat.S_ISGID
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
|
||||||
|
def test_tar_symlink_absolute(self):
|
||||||
|
os.symlink('/etc/passwd', './tests/data/symlink')
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/symlink')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/symlink')
|
||||||
|
tarinfo.linkname = '/etc/passwd'
|
||||||
|
tarinfo.type = tarfile.SYMTYPE
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/symlink')
|
||||||
|
|
||||||
|
def test_tar_symlink_ok(self):
|
||||||
|
shutil.copy('./tests/data/dirty.png', './tests/data/clean.png')
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/dirty.png')
|
||||||
|
t = tarfile.TarInfo('mydir')
|
||||||
|
t.type = tarfile.DIRTYPE
|
||||||
|
zout.addfile(t)
|
||||||
|
zout.add('./tests/data/clean.png')
|
||||||
|
t = tarfile.TarInfo('mylink')
|
||||||
|
t.type = tarfile.SYMTYPE
|
||||||
|
t.linkname = './tests/data/clean.png'
|
||||||
|
zout.addfile(t)
|
||||||
|
zout.add('./tests/data/dirty.jpg')
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.png')
|
||||||
|
|
||||||
|
def test_tar_symlink_relative(self):
|
||||||
|
os.symlink('../../../etc/passwd', './tests/data/symlink')
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('./tests/data/symlink')
|
||||||
|
tarinfo = tarfile.TarInfo('./tests/data/symlink')
|
||||||
|
with open('./tests/data/dirty.jpg', 'rb') as f:
|
||||||
|
zout.addfile(tarinfo=tarinfo, fileobj=f)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/symlink')
|
||||||
|
|
||||||
|
def test_tar_device_file(self):
|
||||||
|
with tarfile.open('./tests/data/clean.tar', 'w') as zout:
|
||||||
|
zout.add('/dev/null')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/clean.tar')
|
||||||
|
os.remove('./tests/data/clean.tar')
|
||||||
|
|
||||||
|
def test_tar_hardlink(self):
|
||||||
|
shutil.copy('./tests/data/dirty.png', './tests/data/clean.png')
|
||||||
|
os.link('./tests/data/clean.png', './tests/data/hardlink.png')
|
||||||
|
with tarfile.open('./tests/data/cleaner.tar', 'w') as zout:
|
||||||
|
zout.add('tests/data/clean.png')
|
||||||
|
zout.add('tests/data/hardlink.png')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
archive.TarParser('./tests/data/cleaner.tar')
|
||||||
|
os.remove('./tests/data/cleaner.tar')
|
||||||
|
os.remove('./tests/data/clean.png')
|
||||||
|
os.remove('./tests/data/hardlink.png')
|
||||||
|
Loading…
Reference in New Issue
Block a user