1 |
From 8d2f7cff76200cbd2337b2cf1707e383eb1fb54b Mon Sep 17 00:00:00 2001 |
2 |
From: Florian Apolloner <florian@apolloner.eu> |
3 |
Date: Fri, 17 Dec 2021 21:07:50 +0100 |
4 |
Subject: [PATCH] [3.2.x] Fixed CVE-2021-45452 -- Fixed potential path |
5 |
traversal in storage subsystem. |
6 |
|
7 |
Thanks to Dennis Brinkrolf for the report. |
8 |
--- |
9 |
django/core/files/storage.py | 9 ++++++++- |
10 |
docs/releases/2.2.26.txt | 9 +++++++++ |
11 |
docs/releases/3.2.11.txt | 9 +++++++++ |
12 |
tests/file_storage/test_generate_filename.py | 19 +++++++++++++------ |
13 |
tests/file_storage/tests.py | 6 ++++++ |
14 |
5 files changed, 45 insertions(+), 7 deletions(-) |
15 |
|
16 |
diff --git a/django/core/files/storage.py b/django/core/files/storage.py |
17 |
index 3e68853b59f8..22984f9498d9 100644 |
18 |
--- a/django/core/files/storage.py |
19 |
+++ b/django/core/files/storage.py |
20 |
@@ -51,7 +51,10 @@ def save(self, name, content, max_length=None): |
21 |
content = File(content, name) |
22 |
|
23 |
name = self.get_available_name(name, max_length=max_length) |
24 |
- return self._save(name, content) |
25 |
+ name = self._save(name, content) |
26 |
+ # Ensure that the name returned from the storage system is still valid. |
27 |
+ validate_file_name(name, allow_relative_path=True) |
28 |
+ return name |
29 |
|
30 |
# These methods are part of the public API, with default implementations. |
31 |
|
32 |
@@ -75,6 +78,7 @@ def get_available_name(self, name, max_length=None): |
33 |
Return a filename that's free on the target storage system and |
34 |
available for new content to be written to. |
35 |
""" |
36 |
+ name = str(name).replace('\\', '/') |
37 |
dir_name, file_name = os.path.split(name) |
38 |
if '..' in pathlib.PurePath(dir_name).parts: |
39 |
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dir_name) |
40 |
@@ -108,6 +112,7 @@ def generate_filename(self, filename): |
41 |
Validate the filename by calling get_valid_name() and return a filename |
42 |
to be passed to the save() method. |
43 |
""" |
44 |
+ filename = str(filename).replace('\\', '/') |
45 |
# `filename` may include a path as returned by FileField.upload_to. |
46 |
dirname, filename = os.path.split(filename) |
47 |
if '..' in pathlib.PurePath(dirname).parts: |
48 |
@@ -297,6 +302,8 @@ def _save(self, name, content): |
49 |
if self.file_permissions_mode is not None: |
50 |
os.chmod(full_path, self.file_permissions_mode) |
51 |
|
52 |
+ # Ensure the saved path is always relative to the storage root. |
53 |
+ name = os.path.relpath(full_path, self.location) |
54 |
# Store filenames with forward slashes, even on Windows. |
55 |
return str(name).replace('\\', '/') |
56 |
|
57 |
diff --git a/tests/file_storage/test_generate_filename.py b/tests/file_storage/test_generate_filename.py |
58 |
index 66551c495b21..78fd0647321a 100644 |
59 |
--- a/tests/file_storage/test_generate_filename.py |
60 |
+++ b/tests/file_storage/test_generate_filename.py |
61 |
@@ -53,13 +53,20 @@ def test_storage_dangerous_paths(self): |
62 |
s.generate_filename(file_name) |
63 |
|
64 |
def test_storage_dangerous_paths_dir_name(self): |
65 |
- file_name = '/tmp/../path' |
66 |
+ candidates = [ |
67 |
+ ('tmp/../path', 'tmp/..'), |
68 |
+ ('tmp\\..\\path', 'tmp/..'), |
69 |
+ ('/tmp/../path', '/tmp/..'), |
70 |
+ ('\\tmp\\..\\path', '/tmp/..'), |
71 |
+ ] |
72 |
s = FileSystemStorage() |
73 |
- msg = "Detected path traversal attempt in '/tmp/..'" |
74 |
- with self.assertRaisesMessage(SuspiciousFileOperation, msg): |
75 |
- s.get_available_name(file_name) |
76 |
- with self.assertRaisesMessage(SuspiciousFileOperation, msg): |
77 |
- s.generate_filename(file_name) |
78 |
+ for file_name, path in candidates: |
79 |
+ msg = "Detected path traversal attempt in '%s'" % path |
80 |
+ with self.subTest(file_name=file_name): |
81 |
+ with self.assertRaisesMessage(SuspiciousFileOperation, msg): |
82 |
+ s.get_available_name(file_name) |
83 |
+ with self.assertRaisesMessage(SuspiciousFileOperation, msg): |
84 |
+ s.generate_filename(file_name) |
85 |
|
86 |
def test_filefield_dangerous_filename(self): |
87 |
candidates = [ |
88 |
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py |
89 |
index 6d17a7118b4e..72380932447f 100644 |
90 |
--- a/tests/file_storage/tests.py |
91 |
+++ b/tests/file_storage/tests.py |
92 |
@@ -297,6 +297,12 @@ def test_file_save_with_path(self): |
93 |
|
94 |
self.storage.delete('path/to/test.file') |
95 |
|
96 |
+ def test_file_save_abs_path(self): |
97 |
+ test_name = 'path/to/test.file' |
98 |
+ f = ContentFile('file saved with path') |
99 |
+ f_name = self.storage.save(os.path.join(self.temp_dir, test_name), f) |
100 |
+ self.assertEqual(f_name, test_name) |
101 |
+ |
102 |
def test_save_doesnt_close(self): |
103 |
with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file: |
104 |
file.write(b'1') |