From 77aafbae422e69fd96fffde95ad4af71bd39ce27 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Fri, 22 Jan 2021 12:23:18 +0100
Subject: [PATCH] [2.2.x] 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.

Backport of bb76e00d125821fcf887c6e0ca3fd081f61d6e5e from master.
---
 django/utils/archive.py                       |  17 +++++++++++---
 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
 8 files changed, 51 insertions(+), 3 deletions(-)
 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

Index: Django-1.8.19/django/utils/archive.py
===================================================================
--- Django-1.8.19.orig/django/utils/archive.py
+++ Django-1.8.19/django/utils/archive.py
@@ -26,6 +26,7 @@ import shutil
 import tarfile
 import zipfile
 
+from django.core.exceptions import SuspiciousOperation
 from django.utils import six
 
 
@@ -125,6 +126,13 @@ class BaseArchive(object):
                 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')
 
@@ -149,7 +157,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)
@@ -190,11 +198,13 @@ class ZipArchive(BaseArchive):
             data = self._archive.read(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)
Index: Django-1.8.19/tests/utils_tests/test_archive.py
===================================================================
--- Django-1.8.19.orig/tests/utils_tests/test_archive.py
+++ Django-1.8.19/tests/utils_tests/test_archive.py
@@ -3,6 +3,8 @@ import shutil
 import tempfile
 import unittest
 
+from django.core.exceptions import SuspiciousOperation
+from django.test import SimpleTestCase
 from django.utils._os import upath
 from django.utils.archive import Archive, extract
 
@@ -74,3 +76,22 @@ class TestGzipTar(ArchiveTester, unittes
 
 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)
