diff --git a/django/utils/archive.py b/django/utils/archive.py index 5b9998f89c..f2f153a1fc 100644 --- a/django/utils/archive.py +++ b/django/utils/archive.py @@ -27,6 +27,8 @@ import stat import tarfile import zipfile +from django.core.exceptions import SuspiciousOperation + class ArchiveException(Exception): """ @@ -133,6 +135,13 @@ class BaseArchive: 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') @@ -155,7 +164,7 @@ class TarArchive(BaseArchive): 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) @@ -198,11 +207,13 @@ class ZipArchive(BaseArchive): 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) diff --git a/docs/releases/2.2.18.txt b/docs/releases/2.2.18.txt new file mode 100644 index 0000000000..45df4fb83c --- /dev/null +++ b/docs/releases/2.2.18.txt @@ -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. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 247b69e797..67131ba1d8 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -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 diff --git a/tests/utils_tests/test_archive.py b/tests/utils_tests/test_archive.py index d58d211ae5..ed7908df82 100644 --- a/tests/utils_tests/test_archive.py +++ b/tests/utils_tests/test_archive.py @@ -5,6 +5,8 @@ import sys 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') @@ -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) diff --git a/tests/utils_tests/traversal_archives/traversal.tar b/tests/utils_tests/traversal_archives/traversal.tar new file mode 100644 index 0000000000..07eede517a Binary files /dev/null and b/tests/utils_tests/traversal_archives/traversal.tar differ diff --git a/tests/utils_tests/traversal_archives/traversal_absolute.tar b/tests/utils_tests/traversal_archives/traversal_absolute.tar new file mode 100644 index 0000000000..231566b069 Binary files /dev/null and b/tests/utils_tests/traversal_archives/traversal_absolute.tar differ diff --git a/tests/utils_tests/traversal_archives/traversal_disk_win.tar b/tests/utils_tests/traversal_archives/traversal_disk_win.tar new file mode 100644 index 0000000000..97f0b95501 Binary files /dev/null and b/tests/utils_tests/traversal_archives/traversal_disk_win.tar differ diff --git a/tests/utils_tests/traversal_archives/traversal_disk_win.zip b/tests/utils_tests/traversal_archives/traversal_disk_win.zip new file mode 100644 index 0000000000..e5ab208398 Binary files /dev/null and b/tests/utils_tests/traversal_archives/traversal_disk_win.zip differ