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

Thanks WhiteSage for the report.

Backport of ea0febbba531a3ecc8c77b570efbfb68ca7155db from master.
This commit is contained in:
Mariusz Felisiak 2020-08-21 11:44:46 +02:00
parent dc39e62e6b
commit 375657a71c
5 changed files with 63 additions and 25 deletions

View File

@ -231,9 +231,9 @@ class FileSystemStorage(Storage):
if not os.path.exists(directory): if not os.path.exists(directory):
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) os.makedirs(directory, self.directory_permissions_mode)
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

@ -7,6 +7,7 @@ import time
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import StringIO from io import StringIO
from pathlib import Path
from urllib.request import urlopen from urllib.request import urlopen
from django.core.cache import cache from django.core.cache import cache
@ -901,16 +902,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 django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders, storage from django.contrib.staticfiles import finders, storage
@ -508,11 +509,18 @@ 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)
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) self.assertEqual(dir_mode, 0o765)
@override_settings( @override_settings(
@ -521,11 +529,18 @@ 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)
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) self.assertEqual(dir_mode, 0o777 & ~self.umask)
@override_settings( @override_settings(
@ -535,11 +550,18 @@ 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)
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) self.assertEqual(dir_mode, 0o740)