A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/django/django/commit/04ac1624bdc2fa737188401757cf95ced122d26d below:

[2.2.x] Fixed CVE-2021-31542 -- Tightened path & file name sanitation… · django/django@04ac162 · GitHub

File tree Expand file treeCollapse file tree 12 files changed

+162

-13

lines changed

Filter options

Expand file treeCollapse file tree 12 files changed

+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