From 924a0c092e65fa2d0953fd1855d2dc8786d94de2 Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:13:36 +0200 Subject: [PATCH] Fixed CVE-2025-59682 -- Fixed potential partial directory-traversal via archive.extract(). Thanks stackered for the report. Follow up to 05413afa8c18cdb978fcdf470e09f7a12b234a23. --- django/utils/archive.py | 6 +++++- docs/releases/4.2.25.txt | 8 ++++++++ docs/releases/5.1.13.txt | 8 ++++++++ docs/releases/5.2.7.txt | 8 ++++++++ tests/utils_tests/test_archive.py | 19 +++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/django/utils/archive.py b/django/utils/archive.py index 4042e89af9..e9c0ce7cb4 100644 --- a/django/utils/archive.py +++ b/django/utils/archive.py @@ -145,7 +145,11 @@ class BaseArchive: 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 5412777055..7ba23c0132 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 96b81c0102..7b9b5c8d39 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 05d03a991e..b8c27d1de2 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 89a45bc072..4d365e4d98 100644 --- a/tests/utils_tests/test_archive.py +++ b/tests/utils_tests/test_archive.py @@ -3,6 +3,7 @@ import stat import sys import tempfile import unittest +import zipfile from django.core.exceptions import SuspiciousOperation from django.test import SimpleTestCase @@ -94,3 +95,21 @@ class TestArchiveInvalid(SimpleTestCase): 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)