VYPR
Moderate severityNVD Advisory· Published Feb 2, 2021· Updated Aug 3, 2024

CVE-2021-3281

CVE-2021-3281

Description

In Django 2.2 before 2.2.18, 3.0 before 3.0.12, and 3.1 before 3.1.6, the django.utils.archive.extract method (used by "startapp --template" and "startproject --template") allows directory traversal via an archive with absolute paths or relative paths with dot segments.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Django 2.2/3.0/3.1's django.utils.archive.extract is vulnerable to directory traversal via crafted archives, allowing arbitrary file overwrite.

Vulnerability

Analysis

The vulnerability resides in django.utils.archive.extract, a utility used by startapp --template and startproject --template commands. It fails to properly sanitize archive entries containing absolute paths or relative paths with dot segments, enabling a directory traversal attack. This affects Django versions 2.2 before 2.2.18, 3.0 before 3.0.12, and 3.1 before 3.1.6. [1][2]

Attack

Vector

An attacker must supply a maliciously crafted archive (e.g., tar or zip) to the vulnerable template extraction process. This requires that a developer or build process unpacks the archive using the affected extract() method. No authentication is needed beyond the ability to provide the archive file. The attack complexity is high due to the requirement of crafting a specific archive with path traversal payloads. The attack vector is local or network-based, depending on how the archive is delivered. [1]

Impact

Successful exploitation allows an attacker to overwrite arbitrary files on the filesystem within the permissions of the Django process. This could lead to code execution if the overwritten file is a Python source file or configuration file subsequently loaded, or to denial of service via destruction of critical data. The vulnerability has a CVSS v3.1 score of 5.5 (Medium) with high impact to integrity. [1][2]

Mitigation

The Django project has released patched versions: 2.2.18, 3.0.12, and 3.1.6. Users should upgrade immediately. No workarounds are documented; avoiding use of untrusted templates is the only temporary mitigation. The vulnerability is not listed in CISA's Known Exploited Vulnerabilities catalog as of the latest update. [4]

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
DjangoPyPI
>= 2.2, < 2.2.182.2.18
DjangoPyPI
>= 3.1, < 3.1.63.1.6
DjangoPyPI
>= 3.0, < 3.0.123.0.12

Affected products

82

Patches

4
52e409ed1728

[3.0.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().

https://github.com/django/djangoMariusz FelisiakJan 22, 2021via ghsa
9 files changed · +67 3
  • django/utils/archive.py+14 3 modified
    @@ -27,6 +27,8 @@
     import tarfile
     import zipfile
     
    +from django.core.exceptions import SuspiciousOperation
    +
     
     class ArchiveException(Exception):
         """
    @@ -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')
     
    @@ -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:
                         os.makedirs(filename, exist_ok=True)
    @@ -198,8 +207,10 @@ 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 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:
    
  • docs/releases/2.2.18.txt+15 0 added
    @@ -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.
    
  • docs/releases/3.0.12.txt+15 0 added
    @@ -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.
    
  • docs/releases/index.txt+2 0 modified
    @@ -25,6 +25,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
    @@ -43,6 +44,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
    
  • tests/utils_tests/test_archive.py+21 0 modified
    @@ -4,6 +4,8 @@
     import tempfile
     import unittest
     
    +from django.core.exceptions import SuspiciousOperation
    +from django.test import SimpleTestCase
     from django.utils import archive
     
     
    @@ -45,3 +47,22 @@ def test_extract_file_permissions(self):
                     # 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)
    
  • tests/utils_tests/traversal_archives/traversal_absolute.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.zip+0 0 added
  • tests/utils_tests/traversal_archives/traversal.tar+0 0 added
21e7622dec1f

[2.2.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().

https://github.com/django/djangoMariusz FelisiakJan 22, 2021via ghsa
8 files changed · +51 3
  • django/utils/archive.py+14 3 modified
    @@ -27,6 +27,8 @@
     import tarfile
     import zipfile
     
    +from django.core.exceptions import SuspiciousOperation
    +
     
     class ArchiveException(Exception):
         """
    @@ -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')
     
    @@ -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)
    @@ -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)
    
  • docs/releases/2.2.18.txt+15 0 added
    @@ -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.
    
  • docs/releases/index.txt+1 0 modified
    @@ -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
    
  • tests/utils_tests/test_archive.py+21 0 modified
    @@ -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')
    @@ -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)
    
  • tests/utils_tests/traversal_archives/traversal_absolute.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.zip+0 0 added
  • tests/utils_tests/traversal_archives/traversal.tar+0 0 added
02e6592835b4

[3.1.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().

https://github.com/django/djangoMariusz FelisiakJan 22, 2021via ghsa
10 files changed · +77 5
  • django/utils/archive.py+14 3 modified
    @@ -27,6 +27,8 @@
     import tarfile
     import zipfile
     
    +from django.core.exceptions import SuspiciousOperation
    +
     
     class ArchiveException(Exception):
         """
    @@ -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')
     
    @@ -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:
                         os.makedirs(filename, exist_ok=True)
    @@ -198,8 +207,10 @@ 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 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:
    
  • docs/releases/2.2.18.txt+15 0 added
    @@ -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.
    
  • docs/releases/3.0.12.txt+15 0 added
    @@ -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.
    
  • docs/releases/3.1.6.txt+10 2 modified
    @@ -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
     ========
    
  • docs/releases/index.txt+2 0 modified
    @@ -38,6 +38,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
    @@ -56,6 +57,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
    
  • tests/utils_tests/test_archive.py+21 0 modified
    @@ -4,6 +4,8 @@
     import tempfile
     import unittest
     
    +from django.core.exceptions import SuspiciousOperation
    +from django.test import SimpleTestCase
     from django.utils import archive
     
     
    @@ -45,3 +47,22 @@ def test_extract_file_permissions(self):
                     # 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)
    
  • tests/utils_tests/traversal_archives/traversal_absolute.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.zip+0 0 added
  • tests/utils_tests/traversal_archives/traversal.tar+0 0 added
05413afa8c18

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

https://github.com/django/djangoMariusz FelisiakJan 22, 2021via ghsa
10 files changed · +77 5
  • django/utils/archive.py+14 3 modified
    @@ -27,6 +27,8 @@
     import tarfile
     import zipfile
     
    +from django.core.exceptions import SuspiciousOperation
    +
     
     class ArchiveException(Exception):
         """
    @@ -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')
     
    @@ -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:
                         os.makedirs(filename, exist_ok=True)
    @@ -198,8 +207,10 @@ 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 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:
    
  • docs/releases/2.2.18.txt+15 0 added
    @@ -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.
    
  • docs/releases/3.0.12.txt+15 0 added
    @@ -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.
    
  • docs/releases/3.1.6.txt+10 2 modified
    @@ -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
     ========
    
  • docs/releases/index.txt+2 0 modified
    @@ -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
    
  • tests/utils_tests/test_archive.py+21 0 modified
    @@ -4,6 +4,8 @@
     import tempfile
     import unittest
     
    +from django.core.exceptions import SuspiciousOperation
    +from django.test import SimpleTestCase
     from django.utils import archive
     
     
    @@ -45,3 +47,22 @@ def test_extract_file_permissions(self):
                     # 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)
    
  • tests/utils_tests/traversal_archives/traversal_absolute.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.tar+0 0 added
  • tests/utils_tests/traversal_archives/traversal_disk_win.zip+0 0 added
  • tests/utils_tests/traversal_archives/traversal.tar+0 0 added

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

18

News mentions

0

No linked articles in our index yet.