Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().

Thanks Florian Apolloner, Shai Berger, and Simon Charette for reviews.

Thanks Wang Baohua for the report.
This commit is contained in:
Mariusz Felisiak 2021-01-22 12:23:18 +01:00
parent 6822aa5c6c
commit 05413afa8c
10 changed files with 77 additions and 5 deletions

View File

@ -27,6 +27,8 @@ import stat
import tarfile import tarfile
import zipfile import zipfile
from django.core.exceptions import SuspiciousOperation
class ArchiveException(Exception): class ArchiveException(Exception):
""" """
@ -133,6 +135,13 @@ class BaseArchive:
return False return False
return True return True
def target_filename(self, to_path, name):
target_path = os.path.abspath(to_path)
filename = os.path.abspath(os.path.join(target_path, name))
if not filename.startswith(target_path):
raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
return filename
def extract(self): def extract(self):
raise NotImplementedError('subclasses of BaseArchive must provide an extract() method') raise NotImplementedError('subclasses of BaseArchive must provide an extract() method')
@ -155,7 +164,7 @@ class TarArchive(BaseArchive):
name = member.name name = member.name
if leading: if leading:
name = self.split_leading_dir(name)[1] name = self.split_leading_dir(name)[1]
filename = os.path.join(to_path, name) filename = self.target_filename(to_path, name)
if member.isdir(): if member.isdir():
if filename: if filename:
os.makedirs(filename, exist_ok=True) os.makedirs(filename, exist_ok=True)
@ -198,8 +207,10 @@ class ZipArchive(BaseArchive):
info = self._archive.getinfo(name) info = self._archive.getinfo(name)
if leading: if leading:
name = self.split_leading_dir(name)[1] name = self.split_leading_dir(name)[1]
filename = os.path.join(to_path, name) if not name:
if filename.endswith(('/', '\\')): continue
filename = self.target_filename(to_path, name)
if name.endswith(('/', '\\')):
# A directory # A directory
os.makedirs(filename, exist_ok=True) os.makedirs(filename, exist_ok=True)
else: else:

15
docs/releases/2.2.18.txt Normal file
View File

@ -0,0 +1,15 @@
===========================
Django 2.2.18 release notes
===========================
*February 1, 2021*
Django 2.2.18 fixes a security issue with severity "low" in 2.2.17.
CVE-2021-3281: Potential directory-traversal via ``archive.extract()``
======================================================================
The ``django.utils.archive.extract()`` function, used by
:option:`startapp --template` and :option:`startproject --template`, allowed
directory-traversal via an archive with absolute paths or relative paths with
dot segments.

15
docs/releases/3.0.12.txt Normal file
View File

@ -0,0 +1,15 @@
===========================
Django 3.0.12 release notes
===========================
*February 1, 2021*
Django 3.0.12 fixes a security issue with severity "low" in 3.0.11.
CVE-2021-3281: Potential directory-traversal via ``archive.extract()``
======================================================================
The ``django.utils.archive.extract()`` function, used by
:option:`startapp --template` and :option:`startproject --template`, allowed
directory-traversal via an archive with absolute paths or relative paths with
dot segments.

View File

@ -2,9 +2,17 @@
Django 3.1.6 release notes Django 3.1.6 release notes
========================== ==========================
*Expected February 1, 2021* *February 1, 2021*
Django 3.1.6 fixes several bugs in 3.1.5. Django 3.1.6 fixes a security issue with severity "low" and a bug in 3.1.5.
CVE-2021-3281: Potential directory-traversal via ``archive.extract()``
======================================================================
The ``django.utils.archive.extract()`` function, used by
:option:`startapp --template` and :option:`startproject --template`, allowed
directory-traversal via an archive with absolute paths or relative paths with
dot segments.
Bugfixes Bugfixes
======== ========

View File

@ -52,6 +52,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
3.0.12
3.0.11 3.0.11
3.0.10 3.0.10
3.0.9 3.0.9
@ -70,6 +71,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
2.2.18
2.2.17 2.2.17
2.2.16 2.2.16
2.2.15 2.2.15

View File

@ -4,6 +4,8 @@ import sys
import tempfile import tempfile
import unittest import unittest
from django.core.exceptions import SuspiciousOperation
from django.test import SimpleTestCase
from django.utils import archive from django.utils import archive
@ -45,3 +47,22 @@ class TestArchive(unittest.TestCase):
# A file is readable even if permission data is missing. # A file is readable even if permission data is missing.
filepath = os.path.join(tmpdir, 'no_permissions') filepath = os.path.join(tmpdir, 'no_permissions')
self.assertEqual(os.stat(filepath).st_mode & mask, 0o666 & ~umask) self.assertEqual(os.stat(filepath).st_mode & mask, 0o666 & ~umask)
class TestArchiveInvalid(SimpleTestCase):
def test_extract_function_traversal(self):
archives_dir = os.path.join(os.path.dirname(__file__), 'traversal_archives')
tests = [
('traversal.tar', '..'),
('traversal_absolute.tar', '/tmp/evil.py'),
]
if sys.platform == 'win32':
tests += [
('traversal_disk_win.tar', 'd:evil.py'),
('traversal_disk_win.zip', 'd:evil.py'),
]
msg = "Archive contains invalid path: '%s'"
for entry, invalid_path in tests:
with self.subTest(entry), tempfile.TemporaryDirectory() as tmpdir:
with self.assertRaisesMessage(SuspiciousOperation, msg % invalid_path):
archive.extract(os.path.join(archives_dir, entry), tmpdir)

Binary file not shown.