From 05413afa8c18cdb978fcdf470e09f7a12b234a23 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 22 Jan 2021 12:23:18 +0100 Subject: [PATCH] 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. --- django/utils/archive.py | 17 +++++++++++--- docs/releases/2.2.18.txt | 15 +++++++++++++ docs/releases/3.0.12.txt | 15 +++++++++++++ docs/releases/3.1.6.txt | 12 ++++++++-- docs/releases/index.txt | 2 ++ tests/utils_tests/test_archive.py | 21 ++++++++++++++++++ .../traversal_archives/traversal.tar | Bin 0 -> 10240 bytes .../traversal_archives/traversal_absolute.tar | Bin 0 -> 10240 bytes .../traversal_archives/traversal_disk_win.tar | Bin 0 -> 10240 bytes .../traversal_archives/traversal_disk_win.zip | Bin 0 -> 312 bytes 10 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 docs/releases/2.2.18.txt create mode 100644 docs/releases/3.0.12.txt create mode 100644 tests/utils_tests/traversal_archives/traversal.tar create mode 100644 tests/utils_tests/traversal_archives/traversal_absolute.tar create mode 100644 tests/utils_tests/traversal_archives/traversal_disk_win.tar create mode 100644 tests/utils_tests/traversal_archives/traversal_disk_win.zip diff --git a/django/utils/archive.py b/django/utils/archive.py index 235809f2ad..d5a0cf0446 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: os.makedirs(filename, exist_ok=True) @@ -198,8 +207,10 @@ 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 filename.endswith(('/', '\\')): + if not name: + continue + filename = self.target_filename(to_path, name) + if name.endswith(('/', '\\')): # A directory os.makedirs(filename, exist_ok=True) else: 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/3.0.12.txt b/docs/releases/3.0.12.txt new file mode 100644 index 0000000000..20d9459fea --- /dev/null +++ b/docs/releases/3.0.12.txt @@ -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. diff --git a/docs/releases/3.1.6.txt b/docs/releases/3.1.6.txt index 2d0a48616d..027d2f3b16 100644 --- a/docs/releases/3.1.6.txt +++ b/docs/releases/3.1.6.txt @@ -2,9 +2,17 @@ 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 ======== diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 68733d9b24..32d4d71567 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -52,6 +52,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 3.0.12 3.0.11 3.0.10 3.0.9 @@ -70,6 +71,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 dc7c4b4ebd..8fdf3ec445 100644 --- a/tests/utils_tests/test_archive.py +++ b/tests/utils_tests/test_archive.py @@ -4,6 +4,8 @@ import sys import tempfile import unittest +from django.core.exceptions import SuspiciousOperation +from django.test import SimpleTestCase from django.utils import archive @@ -45,3 +47,22 @@ class TestArchive(unittest.TestCase): # A file is readable even if permission data is missing. filepath = os.path.join(tmpdir, 'no_permissions') 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) diff --git a/tests/utils_tests/traversal_archives/traversal.tar b/tests/utils_tests/traversal_archives/traversal.tar new file mode 100644 index 0000000000000000000000000000000000000000..07eede517a56bdcfd67d561015eb8764ab7bfd82 GIT binary patch literal 10240 zcmeIwO$x&x5QgD7iYG`MiF%$cA_M{{{Ug17BSpy1qAM4n?`(!RM26=xO;g)6t<)^L zRE%DhrqSDV&!b$Towd$8)u>`sI~T?HnoE_tpZCY*W{jkM9Ok*49{aEP@sCrSq;LNH zYZ#mByUxlv;1IfX?&TfiQLbJ7F1jGb)>&tJ>!0Vp#o!A81Q0*~0R#|0009ILKmY** g5I_I{1Q0*~0R#|0009ILKmY**5I_I{1kNPz0VrH4j{pDw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..231566b0699d48145cb6fa5c0c792c2569417a19 GIT binary patch literal 10240 zcmeIwO$x#=5QgD7N={%WX@1V*qJ=`GMXS`?+X!Md#Z^G@olOEmlHvK%Pg5h6OSeiX z$hO!Nv|Mv5msqdrf{R|0sI|_uVnXG)p4VS5%kgZC^xZhD>;8+M`uiupy3;JDx#@1h zc$n|C2F(G-=*!$+{~)(=z4Q0&mcNTOiqTpFmG}O6{v`!_1Q0*~0R#|0009ILKmY** h5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q7Twfd`<_D7ydv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..97f0b95501c1a67b3f04f8d8736de172af68eb57 GIT binary patch literal 10240 zcmeIwTMEK35QX7cN>-qg*lEDs2)0ltRJ>7lZ&DCiq4*R~{O2WZDEmANS3@ z>o=!Ip-pqabzRMSfBgqJ%JbHLh-Tun&_0W6|GfW&1rG=yfB*srAbxYJLGBBsF;!4N{;?fFk21b?_%nS@*A^@ju zUa0OAtJJd09KC`{xG^B3FpObjl4Hi@3<;=%1Q`B0f|w9Tu|gb0f=Rp#Fq0S