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.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | >= 2.2, < 2.2.18 | 2.2.18 |
DjangoPyPI | >= 3.1, < 3.1.6 | 3.1.6 |
DjangoPyPI | >= 3.0, < 3.0.12 | 3.0.12 |
Affected products
82- Django/Djangodescription
- osv-coords81 versionspkg:bitnami/djangopkg:pypi/djangopkg:rpm/suse/ardana-db&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/ardana-horizon&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/ardana-horizon&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/ardana-horizon&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/ardana-logging&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/ardana-logging&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/ardana-logging&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/ardana-monasca&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/ardana-monasca&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/ardana-monasca&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/ardana-mq&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/ardana-mq&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/ardana-opsconsole-ui&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/ardana-osconfig&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/ardana-osconfig&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/ardana-osconfig&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/crowbar-core&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/crowbar-ha&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/crowbar-openstack&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/crowbar-openstack&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/crowbar-openstack&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/grafana&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/kibana&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/kibana&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/kibana&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/kibana&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/kibana&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/kibana&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/monasca-installer&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/openstack-dashboard&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/openstack-dashboard&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/openstack-manila&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/openstack-manila&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/openstack-neutron&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/openstack-neutron&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/openstack-neutron&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/openstack-neutron&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/openstack-neutron&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/openstack-neutron-doc&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/openstack-neutron-doc&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/openstack-neutron-doc&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/openstack-neutron-gbp&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/openstack-neutron-gbp&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/openstack-nova&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/openstack-nova&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/openstack-nova&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/openstack-nova&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/openstack-nova&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/openstack-nova-doc&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/openstack-nova-doc&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/openstack-nova-doc&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/python-Django1&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/python-Django1&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/python-Django&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/python-Django&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/python-Django&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/python-Django&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/python-py&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/release-notes-hpe-helion-openstack&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/release-notes-suse-openstack-cloud&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/release-notes-suse-openstack-cloud&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/release-notes-suse-openstack-cloud&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/release-notes-suse-openstack-cloud&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/rubygem-activerecord-session_store&distro=SUSE%20OpenStack%20Cloud%207pkg:rpm/suse/sleshammer&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/sleshammer&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/spark&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/spark&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/spark&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/venv-openstack-horizon&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/venv-openstack-horizon&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/venv-openstack-horizon-hpe&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/venv-openstack-manila&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/venv-openstack-neutron&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/venv-openstack-neutron&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/venv-openstack-neutron&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/venv-openstack-nova&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/venv-openstack-nova&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/venv-openstack-nova&distro=SUSE%20OpenStack%20Cloud%209
>= 2.2.0, < 2.2.18+ 80 more
- (no CPE)range: >= 2.2.0, < 2.2.18
- (no CPE)range: >= 2.2, < 2.2.18
- (no CPE)range: < 9.0+git.1611600773.5f1de5f-3.22.1
- (no CPE)range: < 8.0+git.1610733160.0f577f4-3.21.1
- (no CPE)range: < 8.0+git.1610733160.0f577f4-3.21.1
- (no CPE)range: < 9.0+git.1610491814.38661c2-3.16.1
- (no CPE)range: < 8.0+git.1610573640.452aed1-3.27.1
- (no CPE)range: < 8.0+git.1610573640.452aed1-3.27.1
- (no CPE)range: < 9.0+git.1610490922.d5f9813-3.16.1
- (no CPE)range: < 8.0+git.1610740501.5dca121-3.27.1
- (no CPE)range: < 8.0+git.1610740501.5dca121-3.27.1
- (no CPE)range: < 9.0+git.1610547641.d79ecfd-3.22.1
- (no CPE)range: < 8.0+git.1605176800.52cccfa-3.29.1
- (no CPE)range: < 8.0+git.1605176800.52cccfa-3.29.1
- (no CPE)range: < 9.0+git.1611867924.eb82818-4.16.1
- (no CPE)range: < 8.0+git.1610643571.91b88d6-3.52.1
- (no CPE)range: < 8.0+git.1610643571.91b88d6-3.52.1
- (no CPE)range: < 9.0+git.1610634027.5934cf8-3.25.1
- (no CPE)range: < 6.0+git.1611320924.849e748ff-3.34.1
- (no CPE)range: < 5.0+git.1610564036.b75ee1b-3.35.1
- (no CPE)range: < 4.0+git.1616146720.44daffca0-9.81.2
- (no CPE)range: < 5.0+git.1610402513.08dca931e-4.49.1
- (no CPE)range: < 6.0+git.1610402342.21499240d-3.31.1
- (no CPE)range: < 6.7.4-1.24.2
- (no CPE)range: < 4.6.3-3.6.1
- (no CPE)range: < 4.6.6-9.2
- (no CPE)range: < 4.6.3-3.6.1
- (no CPE)range: < 4.6.3-4.6.1
- (no CPE)range: < 4.6.3-3.6.1
- (no CPE)range: < 4.6.3-4.6.1
- (no CPE)range: < 20180608_12.47-16.2
- (no CPE)range: < 14.1.1~dev10-3.21.3
- (no CPE)range: < 14.1.1~dev10-3.21.3
- (no CPE)range: < 7.4.2~dev60-4.33.2
- (no CPE)range: < 7.4.2~dev60-4.33.2
- (no CPE)range: < 11.0.9~dev69-3.40.1
- (no CPE)range: < 11.0.9~dev69-3.40.1
- (no CPE)range: < 13.0.8~dev147-3.34.2
- (no CPE)range: < 11.0.9~dev69-3.40.1
- (no CPE)range: < 13.0.8~dev147-3.34.2
- (no CPE)range: < 11.0.9~dev69-3.40.1
- (no CPE)range: < 11.0.9~dev69-3.40.1
- (no CPE)range: < 11.0.9~dev69-3.40.1
- (no CPE)range: < 12.0.1~dev16-3.22.2
- (no CPE)range: < 12.0.1~dev16-3.22.2
- (no CPE)range: < 16.1.9~dev78-3.45.1
- (no CPE)range: < 16.1.9~dev78-3.45.1
- (no CPE)range: < 18.3.1~dev78-3.34.2
- (no CPE)range: < 16.1.9~dev78-3.45.1
- (no CPE)range: < 18.3.1~dev78-3.34.2
- (no CPE)range: < 16.1.9~dev78-3.45.1
- (no CPE)range: < 16.1.9~dev78-3.45.1
- (no CPE)range: < 16.1.9~dev78-3.45.1
- (no CPE)range: < 1.11.29-3.18.2
- (no CPE)range: < 1.11.29-3.18.2
- (no CPE)range: < 1.11.29-3.22.1
- (no CPE)range: < 1.8.19-3.29.1
- (no CPE)range: < 1.11.29-3.22.1
- (no CPE)range: < 1.11.29-3.22.1
- (no CPE)range: < 1.8.1-11.16.2
- (no CPE)range: < 8.20201214-3.29.1
- (no CPE)range: < 8.20201214-3.29.1
- (no CPE)range: < 9.20201214-3.27.2
- (no CPE)range: < 8.20201214-3.29.1
- (no CPE)range: < 9.20201214-3.27.2
- (no CPE)range: < 0.1.2-3.4.2
- (no CPE)range: < 0.8.0-0.20.2
- (no CPE)range: < 0.9.0-7.6.1
- (no CPE)range: < 1.6.3-8.6.1
- (no CPE)range: < 1.6.3-8.6.1
- (no CPE)range: < 1.6.3-8.6.1
- (no CPE)range: < 12.0.5~dev6-14.34.3
- (no CPE)range: < 14.1.1~dev10-4.25.2
- (no CPE)range: < 12.0.5~dev6-14.34.1
- (no CPE)range: < 7.4.2~dev60-3.27.2
- (no CPE)range: < 11.0.9~dev69-13.36.1
- (no CPE)range: < 11.0.9~dev69-13.36.1
- (no CPE)range: < 13.0.8~dev147-6.25.2
- (no CPE)range: < 16.1.9~dev78-11.34.1
- (no CPE)range: < 16.1.9~dev78-11.34.1
- (no CPE)range: < 18.3.1~dev78-3.25.2
Patches
452e409ed1728[3.0.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().
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 addedtests/utils_tests/traversal_archives/traversal_disk_win.tar+0 −0 addedtests/utils_tests/traversal_archives/traversal_disk_win.zip+0 −0 addedtests/utils_tests/traversal_archives/traversal.tar+0 −0 added
21e7622dec1f[2.2.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().
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 addedtests/utils_tests/traversal_archives/traversal_disk_win.tar+0 −0 addedtests/utils_tests/traversal_archives/traversal_disk_win.zip+0 −0 addedtests/utils_tests/traversal_archives/traversal.tar+0 −0 added
02e6592835b4[3.1.x] Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().
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 addedtests/utils_tests/traversal_archives/traversal_disk_win.tar+0 −0 addedtests/utils_tests/traversal_archives/traversal_disk_win.zip+0 −0 addedtests/utils_tests/traversal_archives/traversal.tar+0 −0 added
05413afa8c18Fixed CVE-2021-3281 -- Fixed potential directory-traversal via archive.extract().
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 addedtests/utils_tests/traversal_archives/traversal_disk_win.tar+0 −0 addedtests/utils_tests/traversal_archives/traversal_disk_win.zip+0 −0 addedtests/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- github.com/advisories/GHSA-fvgf-6h6h-3322ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/YF52FKEH5S2P5CM4X7IXSYG67YY2CDOO/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2021-3281ghsaADVISORY
- docs.djangoproject.com/en/3.1/releases/3.0.12ghsaWEB
- docs.djangoproject.com/en/3.1/releases/securityghsaWEB
- docs.djangoproject.com/en/3.1/releases/security/mitrex_refsource_MISC
- github.com/django/django/commit/02e6592835b4559909aa3aaaf67988fef435f624ghsaWEB
- github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23ghsaWEB
- github.com/django/django/commit/21e7622dec1f8612c85c2fc37fe8efbfd3311e37ghsaWEB
- github.com/django/django/commit/52e409ed17287e9aabda847b6afe58be2fa9f86aghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2021-9.yamlghsaWEB
- groups.google.com/forum/ghsaWEB
- groups.google.com/forum/mitrex_refsource_MISC
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/YF52FKEH5S2P5CM4X7IXSYG67YY2CDOOghsaWEB
- security.netapp.com/advisory/ntap-20210226-0004ghsaWEB
- security.netapp.com/advisory/ntap-20210226-0004/mitrex_refsource_CONFIRM
- www.djangoproject.com/weblog/2021/feb/01/security-releasesghsaWEB
- www.djangoproject.com/weblog/2021/feb/01/security-releases/mitrex_refsource_CONFIRM
News mentions
0No linked articles in our index yet.