Directory traversal / path traversal via archive.extract()

HIGH
django/django
Commit: 924a0c092e65
Affected: 4.2.x: <= 4.2.24; 5.1.x: <= 5.1.12; 5.2.x: <= 5.2.6
2026-04-05 13:11 UTC

Description

The commit fixes a partial directory-traversal vulnerability in django.utils.archive.extract() (CVE-2025-59682). Previously, the code performed a naive path check using filename.startswith(target_path) after joining the archive entry name with the target directory. This could be bypassed when an archive member path started with the target directory name but resolved outside the target (e.g., an absolute path inside the archive like /tmp/evil.txt or a path such as /tmp/baseevil.txt). The patch replaces the check with os.path.commonpath([target_path, filename]) and raises SuspiciousOperation if the computed path is not within the target directory, effectively preventing extraction outside the intended directory. The change addresses partial directory traversal during archive extraction and is reflected in releases for affected branches (4.2, 5.1, 5.2).

Proof of Concept

import zipfile, os, tempfile from django.utils import archive from django.core.exceptions import SuspiciousOperation def run_poc(): # Demonstrates exploitation attempt using an absolute path inside the archive with tempfile.TemporaryDirectory() as target_dir: zip_path = os.path.join(target_dir, 'attack.zip') # Create a tiny archive that contains an absolute path entry outside the target dir with zipfile.ZipFile(zip_path, 'w') as zf: zf.writestr('/tmp/evil_outside.txt', 'evil') try: archive.extract(zip_path, target_dir) except SuspiciousOperation: print('Exploit blocked by fix (SuspiciousOperation raised)') else: print('Archive.extract wrote outside the target directory (vulnerability existed pre-fix)') if __name__ == '__main__': run_poc()

Commit Details

Author: Sarah Boyce

Date: 2025-09-16 15:13 UTC

Message:

Fixed CVE-2025-59682 -- Fixed potential partial directory-traversal via archive.extract(). Thanks stackered for the report. Follow up to 05413afa8c18cdb978fcdf470e09f7a12b234a23.

Triage Assessment

Vulnerability Type: Directory Traversal

Confidence: HIGH

Reasoning:

The patch adds validation to archive extraction to prevent partial directory traversal by ensuring extracted paths stay within the target directory (using os.path.commonpath) and raises SuspiciousOperation on invalid paths. This directly addresses CVE-2025-59682.

Verification Assessment

Vulnerability Type: Directory traversal / path traversal via archive.extract()

Confidence: HIGH

Affected Versions: 4.2.x: <= 4.2.24; 5.1.x: <= 5.1.12; 5.2.x: <= 5.2.6

Code Diff

diff --git a/django/utils/archive.py b/django/utils/archive.py index 4042e89af943..e9c0ce7cb435 100644 --- a/django/utils/archive.py +++ b/django/utils/archive.py @@ -145,7 +145,11 @@ def has_leading_dir(self, paths): 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): + try: + if os.path.commonpath([target_path, filename]) != target_path: + raise SuspiciousOperation("Archive contains invalid path: '%s'" % name) + except ValueError: + # Different drives on Windows raises ValueError. raise SuspiciousOperation("Archive contains invalid path: '%s'" % name) return filename diff --git a/docs/releases/4.2.25.txt b/docs/releases/4.2.25.txt index 5412777055bf..7ba23c01323a 100644 --- a/docs/releases/4.2.25.txt +++ b/docs/releases/4.2.25.txt @@ -15,3 +15,11 @@ CVE-2025-59681: Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to :cve:`2022-28346`). + +CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()`` +=============================================================================== + +The ``django.utils.archive.extract()`` function, used by +:option:`startapp --template` and :option:`startproject --template`, allowed +partial directory-traversal via an archive with file paths sharing a common +prefix with the target directory (follow up to :cve:`2021-3281`). diff --git a/docs/releases/5.1.13.txt b/docs/releases/5.1.13.txt index 96b81c0102e8..7b9b5c8d3938 100644 --- a/docs/releases/5.1.13.txt +++ b/docs/releases/5.1.13.txt @@ -15,3 +15,11 @@ CVE-2025-59681: Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to :cve:`2022-28346`). + +CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()`` +=============================================================================== + +The ``django.utils.archive.extract()`` function, used by +:option:`startapp --template` and :option:`startproject --template`, allowed +partial directory-traversal via an archive with file paths sharing a common +prefix with the target directory (follow up to :cve:`2021-3281`). diff --git a/docs/releases/5.2.7.txt b/docs/releases/5.2.7.txt index 05d03a991e90..b8c27d1de244 100644 --- a/docs/releases/5.2.7.txt +++ b/docs/releases/5.2.7.txt @@ -17,6 +17,14 @@ to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to :cve:`2022-28346`). +CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()`` +=============================================================================== + +The ``django.utils.archive.extract()`` function, used by +:option:`startapp --template` and :option:`startproject --template`, allowed +partial directory-traversal via an archive with file paths sharing a common +prefix with the target directory (follow up to :cve:`2021-3281`). + Bugfixes ======== diff --git a/tests/utils_tests/test_archive.py b/tests/utils_tests/test_archive.py index 89a45bc072ad..4d365e4d9825 100644 --- a/tests/utils_tests/test_archive.py +++ b/tests/utils_tests/test_archive.py @@ -3,6 +3,7 @@ import sys import tempfile import unittest +import zipfile from django.core.exceptions import SuspiciousOperation from django.test import SimpleTestCase @@ -94,3 +95,21 @@ def test_extract_function_traversal(self): with self.subTest(entry), tempfile.TemporaryDirectory() as tmpdir: with self.assertRaisesMessage(SuspiciousOperation, msg % invalid_path): archive.extract(os.path.join(archives_dir, entry), tmpdir) + + def test_extract_function_traversal_startswith(self): + with tempfile.TemporaryDirectory() as tmpdir: + base = os.path.abspath(tmpdir) + tarfile_handle = tempfile.NamedTemporaryFile(suffix=".zip", delete=False) + tar_path = tarfile_handle.name + tarfile_handle.close() + self.addCleanup(os.remove, tar_path) + + malicious_member = os.path.join(base + "abc", "evil.txt") + with zipfile.ZipFile(tar_path, "w") as zf: + zf.writestr(malicious_member, "evil\n") + zf.writestr("test.txt", "data\n") + + with self.assertRaisesMessage( + SuspiciousOperation, "Archive contains invalid path" + ): + archive.extract(tar_path, base)
← Back to Alerts View on GitHub →