Skip to content

Commit 8d72715

Browse files
felixxmcarltongibson
authored andcommitted
Fixed CVE-2020-24583, #31921 -- Fixed permissions on intermediate-level static and storage directories on Python 3.7+.
Thanks WhiteSage for the report.
1 parent 2bc38bc commit 8d72715

File tree

7 files changed

+86
-27
lines changed

7 files changed

+86
-27
lines changed

django/core/files/storage.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,9 @@ def _save(self, name, content):
237237
directory = os.path.dirname(full_path)
238238
try:
239239
if self.directory_permissions_mode is not None:
240-
# os.makedirs applies the global umask, so we reset it,
241-
# for consistency with file_permissions_mode behavior.
242-
old_umask = os.umask(0)
240+
# Set the umask because os.makedirs() doesn't apply the "mode"
241+
# argument to intermediate-level directories.
242+
old_umask = os.umask(0o777 & ~self.directory_permissions_mode)
243243
try:
244244
os.makedirs(directory, self.directory_permissions_mode, exist_ok=True)
245245
finally:

docs/releases/2.2.16.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ Django 2.2.16 release notes
44

55
*Expected September 1, 2020*
66

7-
Django 2.2.16 fixes two data loss bugs in 2.2.15.
7+
Django 2.2.16 fixes a security issue and two data loss bugs in 2.2.15.
8+
9+
CVE-2020-24583: Incorrect permissions on intermediate-level directories on Python 3.7+
10+
======================================================================================
11+
12+
On Python 3.7+, :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` mode was not
13+
applied to intermediate-level directories created in the process of uploading
14+
files and to intermediate-level collected static directories when using the
15+
:djadmin:`collectstatic` management command.
16+
17+
You should review and manually fix permissions on existing intermediate-level
18+
directories.
819

920
Bugfixes
1021
========

docs/releases/3.0.10.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ Django 3.0.10 release notes
44

55
*Expected September 1, 2020*
66

7-
Django 3.0.10 fixes two data loss bugs in 3.0.9.
7+
Django 3.0.10 fixes a security issue and two data loss bugs in 3.0.9.
8+
9+
CVE-2020-24583: Incorrect permissions on intermediate-level directories on Python 3.7+
10+
======================================================================================
11+
12+
On Python 3.7+, :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` mode was not
13+
applied to intermediate-level directories created in the process of uploading
14+
files and to intermediate-level collected static directories when using the
15+
:djadmin:`collectstatic` management command.
16+
17+
You should review and manually fix permissions on existing intermediate-level
18+
directories.
819

920
Bugfixes
1021
========

docs/releases/3.1.1.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ Django 3.1.1 release notes
44

55
*Expected September 1, 2020*
66

7-
Django 3.1.1 fixes several bugs in 3.1.
7+
Django 3.1.1 fixes a security issue and several bugs in 3.1.
8+
9+
CVE-2020-24583: Incorrect permissions on intermediate-level directories on Python 3.7+
10+
======================================================================================
11+
12+
On Python 3.7+, :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` mode was not
13+
applied to intermediate-level directories created in the process of uploading
14+
files and to intermediate-level collected static directories when using the
15+
:djadmin:`collectstatic` management command.
16+
17+
You should review and manually fix permissions on existing intermediate-level
18+
directories.
819

920
Bugfixes
1021
========

tests/file_storage/tests.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -972,16 +972,19 @@ def test_file_upload_default_permissions(self):
972972
@override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o765)
973973
def test_file_upload_directory_permissions(self):
974974
self.storage = FileSystemStorage(self.storage_dir)
975-
name = self.storage.save("the_directory/the_file", ContentFile("data"))
976-
dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777
977-
self.assertEqual(dir_mode, 0o765)
975+
name = self.storage.save('the_directory/subdir/the_file', ContentFile('data'))
976+
file_path = Path(self.storage.path(name))
977+
self.assertEqual(file_path.parent.stat().st_mode & 0o777, 0o765)
978+
self.assertEqual(file_path.parent.parent.stat().st_mode & 0o777, 0o765)
978979

979980
@override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=None)
980981
def test_file_upload_directory_default_permissions(self):
981982
self.storage = FileSystemStorage(self.storage_dir)
982-
name = self.storage.save("the_directory/the_file", ContentFile("data"))
983-
dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777
984-
self.assertEqual(dir_mode, 0o777 & ~self.umask)
983+
name = self.storage.save('the_directory/subdir/the_file', ContentFile('data'))
984+
file_path = Path(self.storage.path(name))
985+
expected_mode = 0o777 & ~self.umask
986+
self.assertEqual(file_path.parent.stat().st_mode & 0o777, expected_mode)
987+
self.assertEqual(file_path.parent.parent.stat().st_mode & 0o777, expected_mode)
985988

986989

987990
class FileStoragePathParsing(SimpleTestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
html {height: 100%;}

tests/staticfiles_tests/test_storage.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import tempfile
55
import unittest
66
from io import StringIO
7+
from pathlib import Path
78
from unittest import mock
89

910
from django.conf import settings
@@ -457,25 +458,39 @@ def run_collectstatic(self, **kwargs):
457458
)
458459
def test_collect_static_files_permissions(self):
459460
call_command('collectstatic', **self.command_params)
460-
test_file = os.path.join(settings.STATIC_ROOT, "test.txt")
461-
test_dir = os.path.join(settings.STATIC_ROOT, "subdir")
462-
file_mode = os.stat(test_file)[0] & 0o777
463-
dir_mode = os.stat(test_dir)[0] & 0o777
461+
static_root = Path(settings.STATIC_ROOT)
462+
test_file = static_root / 'test.txt'
463+
file_mode = test_file.stat().st_mode & 0o777
464464
self.assertEqual(file_mode, 0o655)
465-
self.assertEqual(dir_mode, 0o765)
465+
tests = [
466+
static_root / 'subdir',
467+
static_root / 'nested',
468+
static_root / 'nested' / 'css',
469+
]
470+
for directory in tests:
471+
with self.subTest(directory=directory):
472+
dir_mode = directory.stat().st_mode & 0o777
473+
self.assertEqual(dir_mode, 0o765)
466474

467475
@override_settings(
468476
FILE_UPLOAD_PERMISSIONS=None,
469477
FILE_UPLOAD_DIRECTORY_PERMISSIONS=None,
470478
)
471479
def test_collect_static_files_default_permissions(self):
472480
call_command('collectstatic', **self.command_params)
473-
test_file = os.path.join(settings.STATIC_ROOT, "test.txt")
474-
test_dir = os.path.join(settings.STATIC_ROOT, "subdir")
475-
file_mode = os.stat(test_file)[0] & 0o777
476-
dir_mode = os.stat(test_dir)[0] & 0o777
481+
static_root = Path(settings.STATIC_ROOT)
482+
test_file = static_root / 'test.txt'
483+
file_mode = test_file.stat().st_mode & 0o777
477484
self.assertEqual(file_mode, 0o666 & ~self.umask)
478-
self.assertEqual(dir_mode, 0o777 & ~self.umask)
485+
tests = [
486+
static_root / 'subdir',
487+
static_root / 'nested',
488+
static_root / 'nested' / 'css',
489+
]
490+
for directory in tests:
491+
with self.subTest(directory=directory):
492+
dir_mode = directory.stat().st_mode & 0o777
493+
self.assertEqual(dir_mode, 0o777 & ~self.umask)
479494

480495
@override_settings(
481496
FILE_UPLOAD_PERMISSIONS=0o655,
@@ -484,12 +499,19 @@ def test_collect_static_files_default_permissions(self):
484499
)
485500
def test_collect_static_files_subclass_of_static_storage(self):
486501
call_command('collectstatic', **self.command_params)
487-
test_file = os.path.join(settings.STATIC_ROOT, "test.txt")
488-
test_dir = os.path.join(settings.STATIC_ROOT, "subdir")
489-
file_mode = os.stat(test_file)[0] & 0o777
490-
dir_mode = os.stat(test_dir)[0] & 0o777
502+
static_root = Path(settings.STATIC_ROOT)
503+
test_file = static_root / 'test.txt'
504+
file_mode = test_file.stat().st_mode & 0o777
491505
self.assertEqual(file_mode, 0o640)
492-
self.assertEqual(dir_mode, 0o740)
506+
tests = [
507+
static_root / 'subdir',
508+
static_root / 'nested',
509+
static_root / 'nested' / 'css',
510+
]
511+
for directory in tests:
512+
with self.subTest(directory=directory):
513+
dir_mode = directory.stat().st_mode & 0o777
514+
self.assertEqual(dir_mode, 0o740)
493515

494516

495517
@override_settings(

0 commit comments

Comments
 (0)