Fixed CVE-2020-24583, #31921 -- Fixed permissions on intermediate-level static and storage directories on Python 3.7+.

Thanks WhiteSage for the report.
This commit is contained in:
Mariusz Felisiak 2020-08-21 11:44:46 +02:00 committed by Carlton Gibson
parent 2bc38bc7ca
commit 8d7271578d
7 changed files with 86 additions and 27 deletions

View File

@ -237,9 +237,9 @@ class FileSystemStorage(Storage):
directory = os.path.dirname(full_path) directory = os.path.dirname(full_path)
try: try:
if self.directory_permissions_mode is not None: if self.directory_permissions_mode is not None:
# os.makedirs applies the global umask, so we reset it, # Set the umask because os.makedirs() doesn't apply the "mode"
# for consistency with file_permissions_mode behavior. # argument to intermediate-level directories.
old_umask = os.umask(0) old_umask = os.umask(0o777 & ~self.directory_permissions_mode)
try: try:
os.makedirs(directory, self.directory_permissions_mode, exist_ok=True) os.makedirs(directory, self.directory_permissions_mode, exist_ok=True)
finally: finally:

View File

@ -4,7 +4,18 @@ Django 2.2.16 release notes
*Expected September 1, 2020* *Expected September 1, 2020*
Django 2.2.16 fixes two data loss bugs in 2.2.15. Django 2.2.16 fixes a security issue and two data loss bugs in 2.2.15.
CVE-2020-24583: Incorrect permissions on intermediate-level directories on Python 3.7+
======================================================================================
On Python 3.7+, :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` mode was not
applied to intermediate-level directories created in the process of uploading
files and to intermediate-level collected static directories when using the
:djadmin:`collectstatic` management command.
You should review and manually fix permissions on existing intermediate-level
directories.
Bugfixes Bugfixes
======== ========

View File

@ -4,7 +4,18 @@ Django 3.0.10 release notes
*Expected September 1, 2020* *Expected September 1, 2020*
Django 3.0.10 fixes two data loss bugs in 3.0.9. Django 3.0.10 fixes a security issue and two data loss bugs in 3.0.9.
CVE-2020-24583: Incorrect permissions on intermediate-level directories on Python 3.7+
======================================================================================
On Python 3.7+, :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` mode was not
applied to intermediate-level directories created in the process of uploading
files and to intermediate-level collected static directories when using the
:djadmin:`collectstatic` management command.
You should review and manually fix permissions on existing intermediate-level
directories.
Bugfixes Bugfixes
======== ========

View File

@ -4,7 +4,18 @@ Django 3.1.1 release notes
*Expected September 1, 2020* *Expected September 1, 2020*
Django 3.1.1 fixes several bugs in 3.1. Django 3.1.1 fixes a security issue and several bugs in 3.1.
CVE-2020-24583: Incorrect permissions on intermediate-level directories on Python 3.7+
======================================================================================
On Python 3.7+, :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` mode was not
applied to intermediate-level directories created in the process of uploading
files and to intermediate-level collected static directories when using the
:djadmin:`collectstatic` management command.
You should review and manually fix permissions on existing intermediate-level
directories.
Bugfixes Bugfixes
======== ========

View File

@ -972,16 +972,19 @@ class FileStoragePermissions(unittest.TestCase):
@override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o765) @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o765)
def test_file_upload_directory_permissions(self): def test_file_upload_directory_permissions(self):
self.storage = FileSystemStorage(self.storage_dir) self.storage = FileSystemStorage(self.storage_dir)
name = self.storage.save("the_directory/the_file", ContentFile("data")) name = self.storage.save('the_directory/subdir/the_file', ContentFile('data'))
dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777 file_path = Path(self.storage.path(name))
self.assertEqual(dir_mode, 0o765) self.assertEqual(file_path.parent.stat().st_mode & 0o777, 0o765)
self.assertEqual(file_path.parent.parent.stat().st_mode & 0o777, 0o765)
@override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=None) @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=None)
def test_file_upload_directory_default_permissions(self): def test_file_upload_directory_default_permissions(self):
self.storage = FileSystemStorage(self.storage_dir) self.storage = FileSystemStorage(self.storage_dir)
name = self.storage.save("the_directory/the_file", ContentFile("data")) name = self.storage.save('the_directory/subdir/the_file', ContentFile('data'))
dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777 file_path = Path(self.storage.path(name))
self.assertEqual(dir_mode, 0o777 & ~self.umask) expected_mode = 0o777 & ~self.umask
self.assertEqual(file_path.parent.stat().st_mode & 0o777, expected_mode)
self.assertEqual(file_path.parent.parent.stat().st_mode & 0o777, expected_mode)
class FileStoragePathParsing(SimpleTestCase): class FileStoragePathParsing(SimpleTestCase):

View File

@ -0,0 +1 @@
html {height: 100%;}

View File

@ -4,6 +4,7 @@ import sys
import tempfile import tempfile
import unittest import unittest
from io import StringIO from io import StringIO
from pathlib import Path
from unittest import mock from unittest import mock
from django.conf import settings from django.conf import settings
@ -457,12 +458,19 @@ class TestStaticFilePermissions(CollectionTestCase):
) )
def test_collect_static_files_permissions(self): def test_collect_static_files_permissions(self):
call_command('collectstatic', **self.command_params) call_command('collectstatic', **self.command_params)
test_file = os.path.join(settings.STATIC_ROOT, "test.txt") static_root = Path(settings.STATIC_ROOT)
test_dir = os.path.join(settings.STATIC_ROOT, "subdir") test_file = static_root / 'test.txt'
file_mode = os.stat(test_file)[0] & 0o777 file_mode = test_file.stat().st_mode & 0o777
dir_mode = os.stat(test_dir)[0] & 0o777
self.assertEqual(file_mode, 0o655) self.assertEqual(file_mode, 0o655)
self.assertEqual(dir_mode, 0o765) tests = [
static_root / 'subdir',
static_root / 'nested',
static_root / 'nested' / 'css',
]
for directory in tests:
with self.subTest(directory=directory):
dir_mode = directory.stat().st_mode & 0o777
self.assertEqual(dir_mode, 0o765)
@override_settings( @override_settings(
FILE_UPLOAD_PERMISSIONS=None, FILE_UPLOAD_PERMISSIONS=None,
@ -470,12 +478,19 @@ class TestStaticFilePermissions(CollectionTestCase):
) )
def test_collect_static_files_default_permissions(self): def test_collect_static_files_default_permissions(self):
call_command('collectstatic', **self.command_params) call_command('collectstatic', **self.command_params)
test_file = os.path.join(settings.STATIC_ROOT, "test.txt") static_root = Path(settings.STATIC_ROOT)
test_dir = os.path.join(settings.STATIC_ROOT, "subdir") test_file = static_root / 'test.txt'
file_mode = os.stat(test_file)[0] & 0o777 file_mode = test_file.stat().st_mode & 0o777
dir_mode = os.stat(test_dir)[0] & 0o777
self.assertEqual(file_mode, 0o666 & ~self.umask) self.assertEqual(file_mode, 0o666 & ~self.umask)
self.assertEqual(dir_mode, 0o777 & ~self.umask) tests = [
static_root / 'subdir',
static_root / 'nested',
static_root / 'nested' / 'css',
]
for directory in tests:
with self.subTest(directory=directory):
dir_mode = directory.stat().st_mode & 0o777
self.assertEqual(dir_mode, 0o777 & ~self.umask)
@override_settings( @override_settings(
FILE_UPLOAD_PERMISSIONS=0o655, FILE_UPLOAD_PERMISSIONS=0o655,
@ -484,12 +499,19 @@ class TestStaticFilePermissions(CollectionTestCase):
) )
def test_collect_static_files_subclass_of_static_storage(self): def test_collect_static_files_subclass_of_static_storage(self):
call_command('collectstatic', **self.command_params) call_command('collectstatic', **self.command_params)
test_file = os.path.join(settings.STATIC_ROOT, "test.txt") static_root = Path(settings.STATIC_ROOT)
test_dir = os.path.join(settings.STATIC_ROOT, "subdir") test_file = static_root / 'test.txt'
file_mode = os.stat(test_file)[0] & 0o777 file_mode = test_file.stat().st_mode & 0o777
dir_mode = os.stat(test_dir)[0] & 0o777
self.assertEqual(file_mode, 0o640) self.assertEqual(file_mode, 0o640)
self.assertEqual(dir_mode, 0o740) tests = [
static_root / 'subdir',
static_root / 'nested',
static_root / 'nested' / 'css',
]
for directory in tests:
with self.subTest(directory=directory):
dir_mode = directory.stat().st_mode & 0o777
self.assertEqual(dir_mode, 0o740)
@override_settings( @override_settings(