summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Apolloner <florian@apolloner.eu>2021-12-17 21:07:50 +0100
committerCarlton Gibson <carlton.gibson@noumenal.es>2022-01-04 10:19:49 +0100
commit8d2f7cff76200cbd2337b2cf1707e383eb1fb54b (patch)
treef3f00f41e92d751a91f61b164e4951e284d920d5
parentc7fe895bca06daf12cc1670b56eaf72a1ef27a16 (diff)
[3.2.x] Fixed CVE-2021-45452 -- Fixed potential path traversal in storage subsystem.
Thanks to Dennis Brinkrolf for the report.
-rw-r--r--django/core/files/storage.py9
-rw-r--r--docs/releases/2.2.26.txt9
-rw-r--r--docs/releases/3.2.11.txt9
-rw-r--r--tests/file_storage/test_generate_filename.py19
-rw-r--r--tests/file_storage/tests.py6
5 files changed, 45 insertions, 7 deletions
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
index 3e68853b59..22984f9498 100644
--- a/django/core/files/storage.py
+++ b/django/core/files/storage.py
@@ -51,7 +51,10 @@ class Storage:
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.
@@ -75,6 +78,7 @@ class Storage:
Return 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)
@@ -108,6 +112,7 @@ class Storage:
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:
@@ -297,6 +302,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 str(name).replace('\\', '/')
diff --git a/docs/releases/2.2.26.txt b/docs/releases/2.2.26.txt
index 2ed9b32119..7fbdc02089 100644
--- a/docs/releases/2.2.26.txt
+++ b/docs/releases/2.2.26.txt
@@ -36,3 +36,12 @@ As a reminder, all untrusted user input should be validated before use.
This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
+
+CVE-2021-45452: Potential directory-traversal via ``Storage.save()``
+====================================================================
+
+``Storage.save()`` allowed directory-traversal if directly passed suitably
+crafted file names.
+
+This issue has severity "low" according to the :ref:`Django security policy
+<security-disclosure>`.
diff --git a/docs/releases/3.2.11.txt b/docs/releases/3.2.11.txt
index e715ae866f..bb68e14a8e 100644
--- a/docs/releases/3.2.11.txt
+++ b/docs/releases/3.2.11.txt
@@ -36,3 +36,12 @@ As a reminder, all untrusted user input should be validated before use.
This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
+
+CVE-2021-45452: Potential directory-traversal via ``Storage.save()``
+====================================================================
+
+``Storage.save()`` allowed directory-traversal if directly passed suitably
+crafted file names.
+
+This issue has severity "low" according to the :ref:`Django security policy
+<security-disclosure>`.
diff --git a/tests/file_storage/test_generate_filename.py b/tests/file_storage/test_generate_filename.py
index 66551c495b..78fd064732 100644
--- a/tests/file_storage/test_generate_filename.py
+++ b/tests/file_storage/test_generate_filename.py
@@ -53,13 +53,20 @@ class GenerateFilenameStorageTests(SimpleTestCase):
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 = [
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
index 6d17a7118b..7238093244 100644
--- a/tests/file_storage/tests.py
+++ b/tests/file_storage/tests.py
@@ -297,6 +297,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')