Skip to content

Commit 0b79eb3

Browse files
apollo13carltongibson
authored andcommitted
Fixed CVE-2021-31542 -- Tightened path & file name sanitation in file uploads.
1 parent 8de4ca7 commit 0b79eb3

File tree

14 files changed

+190
-13
lines changed

14 files changed

+190
-13
lines changed

django/core/files/storage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import os
2+
import pathlib
23
from datetime import datetime
34
from urllib.parse import urljoin
45

56
from django.conf import settings
67
from django.core.exceptions import SuspiciousFileOperation
78
from django.core.files import File, locks
89
from django.core.files.move import file_move_safe
10+
from django.core.files.utils import validate_file_name
911
from django.core.signals import setting_changed
1012
from django.utils import timezone
1113
from django.utils._os import safe_join
@@ -74,6 +76,9 @@ def get_available_name(self, name, max_length=None):
7476
available for new content to be written to.
7577
"""
7678
dir_name, file_name = os.path.split(name)
79+
if '..' in pathlib.PurePath(dir_name).parts:
80+
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dir_name)
81+
validate_file_name(file_name)
7782
file_root, file_ext = os.path.splitext(file_name)
7883
# If the filename already exists, generate an alternative filename
7984
# until it doesn't exist.
@@ -105,6 +110,8 @@ def generate_filename(self, filename):
105110
"""
106111
# `filename` may include a path as returned by FileField.upload_to.
107112
dirname, filename = os.path.split(filename)
113+
if '..' in pathlib.PurePath(dirname).parts:
114+
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dirname)
108115
return os.path.normpath(os.path.join(dirname, self.get_valid_name(filename)))
109116

110117
def path(self, name):

django/core/files/uploadedfile.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.conf import settings
99
from django.core.files import temp as tempfile
1010
from django.core.files.base import File
11+
from django.core.files.utils import validate_file_name
1112

1213
__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile',
1314
'SimpleUploadedFile')
@@ -47,6 +48,8 @@ def _set_name(self, name):
4748
ext = ext[:255]
4849
name = name[:255 - len(ext)] + ext
4950

51+
name = validate_file_name(name)
52+
5053
self._name = name
5154

5255
name = property(_get_name, _set_name)

django/core/files/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff 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+
117
class FileProxyMixin:
218
"""
319
A mixin class used to forward file methods to an underlaying file

django/db/models/fields/files.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.core.files.base import File
77
from django.core.files.images import ImageFile
88
from django.core.files.storage import Storage, default_storage
9+
from django.core.files.utils import validate_file_name
910
from django.db.models import signals
1011
from django.db.models.fields import Field
1112
from django.db.models.query_utils import DeferredAttribute
@@ -312,6 +313,7 @@ def generate_filename(self, instance, filename):
312313
Until the storage layer, all file paths are expected to be Unix style
313314
(with forward slashes).
314315
"""
316+
filename = validate_file_name(filename)
315317
if callable(self.upload_to):
316318
filename = self.upload_to(instance, filename)
317319
else:

django/http/multipartparser.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import cgi
1010
import collections
1111
import html
12-
import os
1312
from urllib.parse import unquote
1413

1514
from django.conf import settings
@@ -306,10 +305,25 @@ def handle_file_complete(self, old_field_name, counters):
306305
break
307306

308307
def sanitize_file_name(self, file_name):
308+
"""
309+
Sanitize the filename of an upload.
310+
311+
Remove all possible path separators, even though that might remove more
312+
than actually required by the target system. Filenames that could
313+
potentially cause problems (current/parent dir) are also discarded.
314+
315+
It should be noted that this function could still return a "filepath"
316+
like "C:some_file.txt" which is handled later on by the storage layer.
317+
So while this function does sanitize filenames to some extent, the
318+
resulting filename should still be considered as untrusted user input.
319+
"""
309320
file_name = html.unescape(file_name)
310-
# Cleanup Windows-style path separators.
311-
file_name = file_name[file_name.rfind('\\') + 1:].strip()
312-
return os.path.basename(file_name)
321+
file_name = file_name.rsplit('/')[-1]
322+
file_name = file_name.rsplit('\\')[-1]
323+
324+
if file_name in {'', '.', '..'}:
325+
return None
326+
return file_name
313327

314328
IE_sanitize = sanitize_file_name
315329

django/utils/text.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from gzip import GzipFile
55
from io import BytesIO
66

7+
from django.core.exceptions import SuspiciousFileOperation
78
from django.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
89
from django.utils.regex_helper import _lazy_re_compile
910
from django.utils.translation import gettext as _, gettext_lazy, pgettext
@@ -221,7 +222,7 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
221222

222223

223224
@keep_lazy_text
224-
def get_valid_filename(s):
225+
def get_valid_filename(name):
225226
"""
226227
Return the given string converted to a string that can be used for a clean
227228
filename. Remove leading and trailing spaces; convert other spaces to
@@ -230,8 +231,11 @@ def get_valid_filename(s):
230231
>>> get_valid_filename("john's portrait in 2004.jpg")
231232
'johns_portrait_in_2004.jpg'
232233
"""
233-
s = str(s).strip().replace(' ', '_')
234-
return re.sub(r'(?u)[^-\w.]', '', s)
234+
s = str(name).strip().replace(' ', '_')
235+
s = re.sub(r'(?u)[^-\w.]', '', s)
236+
if s in {'', '.', '..'}:
237+
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
238+
return s
235239

236240

237241
@keep_lazy_text

docs/releases/2.2.21.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff 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.

docs/releases/3.1.9.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
==========================
2+
Django 3.1.9 release notes
3+
==========================
4+
5+
*May 4, 2021*
6+
7+
Django 3.1.9 fixes a security issue in 3.1.8.
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.

docs/releases/3.2.1.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22
Django 3.2.1 release notes
33
==========================
44

5-
*Expected May 4, 2021*
5+
*May 4, 2021*
66

7-
Django 3.2.1 fixes several bugs in 3.2.
7+
Django 3.2.1 fixes a security issue and several bugs in 3.2.
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.
818

919
Bugfixes
1020
========

docs/releases/index.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ versions of the documentation contain the release notes for any later releases.
4040
.. toctree::
4141
:maxdepth: 1
4242

43+
3.1.9
4344
3.1.8
4445
3.1.7
4546
3.1.6
@@ -76,6 +77,7 @@ versions of the documentation contain the release notes for any later releases.
7677
.. toctree::
7778
:maxdepth: 1
7879

80+
2.2.21
7981
2.2.20
8082
2.2.19
8183
2.2.18

0 commit comments

Comments
 (0)