File CVE-2021-45452.patch of Package python-Django1
From e63994b1e6ab8b66d9c5fc2f4bf5cd8d09e749e1 Mon Sep 17 00:00:00 2001
From: Florian Apolloner <florian@apolloner.eu>
Date: Fri, 17 Dec 2021 21:07:50 +0100
Subject: [PATCH] [2.2.x] Fixed CVE-2021-45452 -- Fixed potential path
traversal in storage subsystem.
Thanks to Dennis Brinkrolf for the report.
---
django/core/files/storage.py | 9 ++++++++-
docs/releases/2.2.26.txt | 9 +++++++++
tests/file_storage/test_generate_filename.py | 19 +++++++++++++------
tests/file_storage/tests.py | 6 ++++++
4 files changed, 36 insertions(+), 7 deletions(-)
Index: Django-1.11.29/django/core/files/storage.py
===================================================================
--- Django-1.11.29.orig/django/core/files/storage.py
+++ Django-1.11.29/django/core/files/storage.py
@@ -53,7 +53,10 @@ class Storage(object):
content = File(content, name)
name = self.get_available_name(name, max_length=max_length)
- return self._save(name, content)
+ name = self._save(name, content)
+ # Ensure that the name returned from the storage system is still valid.
+ validate_file_name(name, allow_relative_path=True)
+ return name
# These methods are part of the public API, with default implementations.
@@ -69,6 +72,7 @@ class Storage(object):
Returns a filename that's free on the target storage system, and
available for new content to be written to.
"""
+ name = str(name).replace('\\', '/')
dir_name, file_name = os.path.split(name)
if '..' in pathlib.PurePath(dir_name).parts:
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dir_name)
@@ -103,6 +107,7 @@ class Storage(object):
Validate the filename by calling get_valid_name() and return a filename
to be passed to the save() method.
"""
+ filename = str(filename).replace('\\', '/')
# `filename` may include a path as returned by FileField.upload_to.
dirname, filename = os.path.split(filename)
if '..' in pathlib.PurePath(dirname).parts:
@@ -380,6 +385,8 @@ class FileSystemStorage(Storage):
if self.file_permissions_mode is not None:
os.chmod(full_path, self.file_permissions_mode)
+ # Ensure the saved path is always relative to the storage root.
+ name = os.path.relpath(full_path, self.location)
# Store filenames with forward slashes, even on Windows.
return force_text(name.replace('\\', '/'))
Index: Django-1.11.29/tests/file_storage/test_generate_filename.py
===================================================================
--- Django-1.11.29.orig/tests/file_storage/test_generate_filename.py
+++ Django-1.11.29/tests/file_storage/test_generate_filename.py
@@ -54,13 +54,20 @@ class GenerateFilenameStorageTests(Simpl
s.generate_filename(file_name)
def test_storage_dangerous_paths_dir_name(self):
- file_name = '/tmp/../path'
+ candidates = [
+ ('tmp/../path', 'tmp/..'),
+ ('tmp\\..\\path', 'tmp/..'),
+ ('/tmp/../path', '/tmp/..'),
+ ('\\tmp\\..\\path', '/tmp/..'),
+ ]
s = FileSystemStorage()
- msg = "Detected path traversal attempt in '/tmp/..'"
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- s.get_available_name(file_name)
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- s.generate_filename(file_name)
+ for file_name, path in candidates:
+ msg = "Detected path traversal attempt in '%s'" % path
+ with self.subTest(file_name=file_name):
+ with self.assertRaisesMessage(SuspiciousFileOperation, msg):
+ s.get_available_name(file_name)
+ with self.assertRaisesMessage(SuspiciousFileOperation, msg):
+ s.generate_filename(file_name)
def test_filefield_dangerous_filename(self):
candidates = ['..', '.', '', '???', '$.$.$']
Index: Django-1.11.29/tests/file_storage/tests.py
===================================================================
--- Django-1.11.29.orig/tests/file_storage/tests.py
+++ Django-1.11.29/tests/file_storage/tests.py
@@ -342,6 +342,12 @@ class FileStorageTests(SimpleTestCase):
self.storage.delete('path/to/test.file')
+ def test_file_save_abs_path(self):
+ test_name = 'path/to/test.file'
+ f = ContentFile('file saved with path')
+ f_name = self.storage.save(os.path.join(self.temp_dir, test_name), f)
+ self.assertEqual(f_name, test_name)
+
def test_save_doesnt_close(self):
with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file:
file.write(b'1')
Index: Django-1.11.29/django/core/files/utils.py
===================================================================
--- Django-1.11.29.orig/django/core/files/utils.py
+++ Django-1.11.29/django/core/files/utils.py
@@ -1,18 +1,27 @@
import os
+import pathlib
from django.core.exceptions import SuspiciousFileOperation
-def validate_file_name(name):
- if name != os.path.basename(name):
- raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
-
+def validate_file_name(name, allow_relative_path=False):
# Remove potentially dangerous names
- if name in {'', '.', '..'}:
+ if os.path.basename(name) in {'', '.', '..'}:
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
- return name
+ if allow_relative_path:
+ # Use PurePosixPath() because this branch is checked only in
+ # FileField.generate_filename() where all file paths are expected to be
+ # Unix style (with forward slashes).
+ path = pathlib.PurePosixPath(name)
+ if path.is_absolute() or '..' in path.parts:
+ raise SuspiciousFileOperation(
+ "Detected path traversal attempt in '%s'" % name
+ )
+ elif name != os.path.basename(name):
+ raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
+ return name
class FileProxyMixin(object):
"""