+162
-13
lines changedFilter options
+162
-13
lines changed Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
1
1
import os
2
+
import pathlib
2
3
from datetime import datetime
3
4
from urllib.parse import urljoin
4
5
5
6
from django.conf import settings
6
7
from django.core.exceptions import SuspiciousFileOperation
7
8
from django.core.files import File, locks
8
9
from django.core.files.move import file_move_safe
10
+
from django.core.files.utils import validate_file_name
9
11
from django.core.signals import setting_changed
10
12
from django.utils import timezone
11
13
from django.utils._os import safe_join
@@ -66,6 +68,9 @@ def get_available_name(self, name, max_length=None):
66
68
available for new content to be written to.
67
69
"""
68
70
dir_name, file_name = os.path.split(name)
71
+
if '..' in pathlib.PurePath(dir_name).parts:
72
+
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dir_name)
73
+
validate_file_name(file_name)
69
74
file_root, file_ext = os.path.splitext(file_name)
70
75
# If the filename already exists, add an underscore and a random 7
71
76
# character alphanumeric string (before the file extension, if one
@@ -98,6 +103,8 @@ def generate_filename(self, filename):
98
103
"""
99
104
# `filename` may include a path as returned by FileField.upload_to.
100
105
dirname, filename = os.path.split(filename)
106
+
if '..' in pathlib.PurePath(dirname).parts:
107
+
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dirname)
101
108
return os.path.normpath(os.path.join(dirname, self.get_valid_name(filename)))
102
109
103
110
def path(self, name):
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
8
8
from django.conf import settings
9
9
from django.core.files import temp as tempfile
10
10
from django.core.files.base import File
11
+
from django.core.files.utils import validate_file_name
11
12
12
13
__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile',
13
14
'SimpleUploadedFile')
@@ -47,6 +48,8 @@ def _set_name(self, name):
47
48
ext = ext[:255]
48
49
name = name[:255 - len(ext)] + ext
49
50
51
+
name = validate_file_name(name)
52
+
50
53
self._name = name
51
54
52
55
name = property(_get_name, _set_name)
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
1
+
import os
2
+
3
+
from django.core.exceptions import SuspiciousFileOperation
4
+
5
+
6
+
def validate_file_name(name):
7
+
if name != os.path.basename(name):
8
+
raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
9
+
10
+
# Remove potentially dangerous names
11
+
if name in {'', '.', '..'}:
12
+
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
13
+
14
+
return name
15
+
16
+
1
17
class FileProxyMixin:
2
18
"""
3
19
A mixin class used to forward file methods to an underlaying file
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
6
6
from django.core.files.base import File
7
7
from django.core.files.images import ImageFile
8
8
from django.core.files.storage import default_storage
9
+
from django.core.files.utils import validate_file_name
9
10
from django.db.models import signals
10
11
from django.db.models.fields import Field
11
12
from django.utils.translation import gettext_lazy as _
@@ -299,6 +300,7 @@ def generate_filename(self, instance, filename):
299
300
Until the storage layer, all file paths are expected to be Unix style
300
301
(with forward slashes).
301
302
"""
303
+
filename = validate_file_name(filename)
302
304
if callable(self.upload_to):
303
305
filename = self.upload_to(instance, filename)
304
306
else:
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
7
7
import base64
8
8
import binascii
9
9
import cgi
10
-
import os
10
+
import html
11
11
from urllib.parse import unquote
12
12
13
13
from django.conf import settings
@@ -19,7 +19,6 @@
19
19
)
20
20
from django.utils.datastructures import MultiValueDict
21
21
from django.utils.encoding import force_text
22
-
from django.utils.text import unescape_entities
23
22
24
23
__all__ = ('MultiPartParser', 'MultiPartParserError', 'InputStreamExhausted')
25
24
@@ -295,10 +294,25 @@ def handle_file_complete(self, old_field_name, counters):
295
294
break
296
295
297
296
def sanitize_file_name(self, file_name):
298
-
file_name = unescape_entities(file_name)
299
-
# Cleanup Windows-style path separators.
300
-
file_name = file_name[file_name.rfind('\\') + 1:].strip()
301
-
return os.path.basename(file_name)
297
+
"""
298
+
Sanitize the filename of an upload.
299
+
300
+
Remove all possible path separators, even though that might remove more
301
+
than actually required by the target system. Filenames that could
302
+
potentially cause problems (current/parent dir) are also discarded.
303
+
304
+
It should be noted that this function could still return a "filepath"
305
+
like "C:some_file.txt" which is handled later on by the storage layer.
306
+
So while this function does sanitize filenames to some extent, the
307
+
resulting filename should still be considered as untrusted user input.
308
+
"""
309
+
file_name = html.unescape(file_name)
310
+
file_name = file_name.rsplit('/')[-1]
311
+
file_name = file_name.rsplit('\\')[-1]
312
+
313
+
if file_name in {'', '.', '..'}:
314
+
return None
315
+
return file_name
302
316
303
317
IE_sanitize = sanitize_file_name
304
318
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
4
4
from gzip import GzipFile
5
5
from io import BytesIO
6
6
7
+
from django.core.exceptions import SuspiciousFileOperation
7
8
from django.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
8
9
from django.utils.translation import gettext as _, gettext_lazy, pgettext
9
10
@@ -216,7 +217,7 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
216
217
217
218
218
219
@keep_lazy_text
219
-
def get_valid_filename(s):
220
+
def get_valid_filename(name):
220
221
"""
221
222
Return the given string converted to a string that can be used for a clean
222
223
filename. Remove leading and trailing spaces; convert other spaces to
@@ -225,8 +226,11 @@ def get_valid_filename(s):
225
226
>>> get_valid_filename("john's portrait in 2004.jpg")
226
227
'johns_portrait_in_2004.jpg'
227
228
"""
228
-
s = str(s).strip().replace(' ', '_')
229
-
return re.sub(r'(?u)[^-\w.]', '', s)
229
+
s = str(name).strip().replace(' ', '_')
230
+
s = re.sub(r'(?u)[^-\w.]', '', s)
231
+
if s in {'', '.', '..'}:
232
+
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
233
+
return s
230
234
231
235
232
236
@keep_lazy_text
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
1
+
===========================
2
+
Django 2.2.21 release notes
3
+
===========================
4
+
5
+
*May 4, 2021*
6
+
7
+
Django 2.2.21 fixes a security issue in 2.2.20.
8
+
9
+
CVE-2021-31542: Potential directory-traversal via uploaded files
10
+
================================================================
11
+
12
+
``MultiPartParser``, ``UploadedFile``, and ``FieldFile`` allowed
13
+
directory-traversal via uploaded files with suitably crafted file names.
14
+
15
+
In order to mitigate this risk, stricter basename and path sanitation is now
16
+
applied. Specifically, empty file names and paths with dot segments will be
17
+
rejected.
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases.
25
25
.. toctree::
26
26
:maxdepth: 1
27
27
28
+
2.2.21
28
29
2.2.20
29
30
2.2.19
30
31
2.2.18
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
1
1
import os
2
2
3
+
from django.core.exceptions import SuspiciousFileOperation
3
4
from django.core.files.base import ContentFile
4
-
from django.core.files.storage import Storage
5
+
from django.core.files.storage import FileSystemStorage, Storage
5
6
from django.db.models import FileField
6
7
from django.test import SimpleTestCase
7
8
@@ -36,6 +37,44 @@ def generate_filename(self, filename):
36
37
37
38
38
39
class GenerateFilenameStorageTests(SimpleTestCase):
40
+
def test_storage_dangerous_paths(self):
41
+
candidates = [
42
+
('/tmp/..', '..'),
43
+
('/tmp/.', '.'),
44
+
('', ''),
45
+
]
46
+
s = FileSystemStorage()
47
+
msg = "Could not derive file name from '%s'"
48
+
for file_name, base_name in candidates:
49
+
with self.subTest(file_name=file_name):
50
+
with self.assertRaisesMessage(SuspiciousFileOperation, msg % base_name):
51
+
s.get_available_name(file_name)
52
+
with self.assertRaisesMessage(SuspiciousFileOperation, msg % base_name):
53
+
s.generate_filename(file_name)
54
+
55
+
def test_storage_dangerous_paths_dir_name(self):
56
+
file_name = '/tmp/../path'
57
+
s = FileSystemStorage()
58
+
msg = "Detected path traversal attempt in '/tmp/..'"
59
+
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
60
+
s.get_available_name(file_name)
61
+
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
62
+
s.generate_filename(file_name)
63
+
64
+
def test_filefield_dangerous_filename(self):
65
+
candidates = ['..', '.', '', '???', '$.$.$']
66
+
f = FileField(upload_to='some/folder/')
67
+
msg = "Could not derive file name from '%s'"
68
+
for file_name in candidates:
69
+
with self.subTest(file_name=file_name):
70
+
with self.assertRaisesMessage(SuspiciousFileOperation, msg % file_name):
71
+
f.generate_filename(None, file_name)
72
+
73
+
def test_filefield_dangerous_filename_dir(self):
74
+
f = FileField(upload_to='some/folder/')
75
+
msg = "File name '/tmp/path' includes path elements"
76
+
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
77
+
f.generate_filename(None, '/tmp/path')
39
78
40
79
def test_filefield_generate_filename(self):
41
80
f = FileField(upload_to='some/folder/')
Original file line number Diff line number Diff line change
@@ -8,8 +8,9 @@
8
8
from io import BytesIO, StringIO
9
9
from urllib.parse import quote
10
10
11
+
from django.core.exceptions import SuspiciousFileOperation
11
12
from django.core.files import temp as tempfile
12
-
from django.core.files.uploadedfile import SimpleUploadedFile
13
+
from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
13
14
from django.http.multipartparser import (
14
15
MultiPartParser, MultiPartParserError, parse_header,
15
16
)
@@ -37,6 +38,16 @@
37
38
'../hax0rd.txt', # HTML entities.
38
39
]
39
40
41
+
CANDIDATE_INVALID_FILE_NAMES = [
42
+
'/tmp/', # Directory, *nix-style.
43
+
'c:\\tmp\\', # Directory, win-style.
44
+
'/tmp/.', # Directory dot, *nix-style.
45
+
'c:\\tmp\\.', # Directory dot, *nix-style.
46
+
'/tmp/..', # Parent directory, *nix-style.
47
+
'c:\\tmp\\..', # Parent directory, win-style.
48
+
'', # Empty filename.
49
+
]
50
+
40
51
41
52
@override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[])
42
53
class FileUploadTests(TestCase):
@@ -52,6 +63,22 @@ def tearDownClass(cls):
52
63
shutil.rmtree(MEDIA_ROOT)
53
64
super().tearDownClass()
54
65
66
+
def test_upload_name_is_validated(self):
67
+
candidates = [
68
+
'/tmp/',
69
+
'/tmp/..',
70
+
'/tmp/.',
71
+
]
72
+
if sys.platform == 'win32':
73
+
candidates.extend([
74
+
'c:\\tmp\\',
75
+
'c:\\tmp\\..',
76
+
'c:\\tmp\\.',
77
+
])
78
+
for file_name in candidates:
79
+
with self.subTest(file_name=file_name):
80
+
self.assertRaises(SuspiciousFileOperation, UploadedFile, name=file_name)
81
+
55
82
def test_simple_upload(self):
56
83
with open(__file__, 'rb') as fp:
57
84
post_data = {
@@ -631,6 +658,15 @@ def test_sanitize_file_name(self):
631
658
with self.subTest(file_name=file_name):
632
659
self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt')
633
660
661
+
def test_sanitize_invalid_file_name(self):
662
+
parser = MultiPartParser({
663
+
'CONTENT_TYPE': 'multipart/form-data; boundary=_foo',
664
+
'CONTENT_LENGTH': '1',
665
+
}, StringIO('x'), [], 'utf-8')
666
+
for file_name in CANDIDATE_INVALID_FILE_NAMES:
667
+
with self.subTest(file_name=file_name):
668
+
self.assertIsNone(parser.sanitize_file_name(file_name))
669
+
634
670
def test_rfc2231_parsing(self):
635
671
test_data = (
636
672
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
You can’t perform that action at this time.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4