Skip to content

Commit

Permalink
[2.2.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal vi…
Browse files Browse the repository at this point in the history
…a archive.extract().

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

Thanks Wang Baohua for the report.

Backport of 05413af from master.
  • Loading branch information
felixxm committed Feb 1, 2021
1 parent ee9d623 commit 21e7622
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 3 deletions.
17 changes: 14 additions & 3 deletions django/utils/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import tarfile
import zipfile

from django.core.exceptions import SuspiciousOperation


class ArchiveException(Exception):
"""
Expand Down Expand Up @@ -133,6 +135,13 @@ def has_leading_dir(self, paths):
return False
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):
raise NotImplementedError('subclasses of BaseArchive must provide an extract() method')

Expand All @@ -155,7 +164,7 @@ def extract(self, to_path):
name = member.name
if leading:
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 filename and not os.path.exists(filename):
os.makedirs(filename)
Expand Down Expand Up @@ -198,11 +207,13 @@ def extract(self, to_path):
info = self._archive.getinfo(name)
if leading:
name = self.split_leading_dir(name)[1]
filename = os.path.join(to_path, name)
if not name:
continue
filename = self.target_filename(to_path, name)
dirname = os.path.dirname(filename)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
if filename.endswith(('/', '\\')):
if name.endswith(('/', '\\')):
# A directory
if not os.path.exists(filename):
os.makedirs(filename)
Expand Down
15 changes: 15 additions & 0 deletions docs/releases/2.2.18.txt
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/releases/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree::
:maxdepth: 1

2.2.18
2.2.17
2.2.16
2.2.15
Expand Down
21 changes: 21 additions & 0 deletions tests/utils_tests/test_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import tempfile
import unittest

from django.core.exceptions import SuspiciousOperation
from django.test import SimpleTestCase
from django.utils.archive import Archive, extract

TEST_DIR = os.path.join(os.path.dirname(__file__), 'archives')
Expand Down Expand Up @@ -87,3 +89,22 @@ class TestGzipTar(ArchiveTester, unittest.TestCase):

class TestBzip2Tar(ArchiveTester, unittest.TestCase):
archive = 'foobar.tar.bz2'


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):
extract(os.path.join(archives_dir, entry), tmpdir)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 21e7622

Please sign in to comment.