summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorchrista <christa0584@gmail.com>2020-10-27 00:01:03 +0800
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-11-04 20:30:23 +0100
commitc36075ac1dddfa986340b1a5e15fe48833322372 (patch)
treee921149ca40a764145e4ac6ffa37d5e0d9172b73
parentebb08d19424c314c75908bc6048ff57c2f872269 (diff)
Fixed #31983 -- Added system check for file system caches location.
Thanks Johannes Maron and Nick Pope for reviews.
-rw-r--r--django/core/checks/caches.py43
-rw-r--r--docs/ref/checks.txt4
-rw-r--r--docs/topics/cache.txt10
-rw-r--r--tests/check_framework/test_caches.py63
4 files changed, 117 insertions, 3 deletions
diff --git a/django/core/checks/caches.py b/django/core/checks/caches.py
index 0994a5b0e8..6fdecf5a1e 100644
--- a/django/core/checks/caches.py
+++ b/django/core/checks/caches.py
@@ -1,7 +1,10 @@
+import pathlib
+
from django.conf import settings
-from django.core.cache import DEFAULT_CACHE_ALIAS
+from django.core.cache import DEFAULT_CACHE_ALIAS, caches
+from django.core.cache.backends.filebased import FileBasedCache
-from . import Error, Tags, register
+from . import Error, Tags, Warning, register
E001 = Error(
"You must define a '%s' cache in your CACHES setting." % DEFAULT_CACHE_ALIAS,
@@ -14,3 +17,39 @@ def check_default_cache_is_configured(app_configs, **kwargs):
if DEFAULT_CACHE_ALIAS not in settings.CACHES:
return [E001]
return []
+
+
+@register(Tags.caches, deploy=True)
+def check_cache_location_not_exposed(app_configs, **kwargs):
+ errors = []
+ for name in ('MEDIA_ROOT', 'STATIC_ROOT', 'STATICFILES_DIRS'):
+ setting = getattr(settings, name, None)
+ if not setting:
+ continue
+ if name == 'STATICFILES_DIRS':
+ paths = {
+ pathlib.Path(staticfiles_dir).resolve()
+ for staticfiles_dir in setting
+ }
+ else:
+ paths = {pathlib.Path(setting).resolve()}
+ for alias in settings.CACHES:
+ cache = caches[alias]
+ if not isinstance(cache, FileBasedCache):
+ continue
+ cache_path = pathlib.Path(cache._dir).resolve()
+ if any(path == cache_path for path in paths):
+ relation = 'matches'
+ elif any(path in cache_path.parents for path in paths):
+ relation = 'is inside'
+ elif any(cache_path in path.parents for path in paths):
+ relation = 'contains'
+ else:
+ continue
+ errors.append(Warning(
+ f"Your '{alias}' cache configuration might expose your cache "
+ f"or lead to corruption of your data because its LOCATION "
+ f"{relation} {name}.",
+ id='caches.W002',
+ ))
+ return errors
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index 56fde4198a..0ff6d36a25 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -138,6 +138,10 @@ configured:
* **caches.E001**: You must define a ``'default'`` cache in your
:setting:`CACHES` setting.
+* **caches.W002**: Your ``<cache>`` configuration might expose your cache or
+ lead to corruption of your data because its
+ :setting:`LOCATION <CACHES-LOCATION>` matches/is inside/contains
+ :setting:`MEDIA_ROOT`/:setting:`STATIC_ROOT`/:setting:`STATICFILES_DIRS`.
Database
--------
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index f8e86ae85f..14a257b1f4 100644
--- a/docs/topics/cache.txt
+++ b/docs/topics/cache.txt
@@ -293,6 +293,16 @@ above example, if your server runs as the user ``apache``, make sure the
directory ``/var/tmp/django_cache`` exists and is readable and writable by the
user ``apache``.
+.. warning::
+
+ When the cache :setting:`LOCATION <CACHES-LOCATION>` is contained within
+ :setting:`MEDIA_ROOT`, :setting:`STATIC_ROOT`, or
+ :setting:`STATICFILES_FINDERS`, sensitive data may be exposed.
+
+ An attacker who gains access to the cache file can not only falsify HTML
+ content, which your site will trust, but also remotely execute arbitrary
+ code, as the data is serialized using :mod:`pickle`.
+
.. _local-memory-caching:
Local-memory caching
diff --git a/tests/check_framework/test_caches.py b/tests/check_framework/test_caches.py
index ed21e4710a..002528d1f5 100644
--- a/tests/check_framework/test_caches.py
+++ b/tests/check_framework/test_caches.py
@@ -1,4 +1,9 @@
-from django.core.checks.caches import E001, check_default_cache_is_configured
+import pathlib
+
+from django.core.checks import Warning
+from django.core.checks.caches import (
+ E001, check_cache_location_not_exposed, check_default_cache_is_configured,
+)
from django.test import SimpleTestCase
from django.test.utils import override_settings
@@ -28,3 +33,59 @@ class CheckCacheSettingsAppDirsTest(SimpleTestCase):
Error if 'default' not present in CACHES setting.
"""
self.assertEqual(check_default_cache_is_configured(None), [E001])
+
+
+class CheckCacheLocationTest(SimpleTestCase):
+ warning_message = (
+ "Your 'default' cache configuration might expose your cache or lead "
+ "to corruption of your data because its LOCATION %s %s."
+ )
+
+ @staticmethod
+ def get_settings(setting, cache_path, setting_path):
+ return {
+ 'CACHES': {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
+ 'LOCATION': cache_path,
+ },
+ },
+ setting: [setting_path] if setting == 'STATICFILES_DIRS' else setting_path,
+ }
+
+ def test_cache_path_matches_media_static_setting(self):
+ root = pathlib.Path.cwd()
+ for setting in ('MEDIA_ROOT', 'STATIC_ROOT', 'STATICFILES_DIRS'):
+ settings = self.get_settings(setting, root, root)
+ with self.subTest(setting=setting), self.settings(**settings):
+ msg = self.warning_message % ('matches', setting)
+ self.assertEqual(check_cache_location_not_exposed(None), [
+ Warning(msg, id='caches.W002'),
+ ])
+
+ def test_cache_path_inside_media_static_setting(self):
+ root = pathlib.Path.cwd()
+ for setting in ('MEDIA_ROOT', 'STATIC_ROOT', 'STATICFILES_DIRS'):
+ settings = self.get_settings(setting, root / 'cache', root)
+ with self.subTest(setting=setting), self.settings(**settings):
+ msg = self.warning_message % ('is inside', setting)
+ self.assertEqual(check_cache_location_not_exposed(None), [
+ Warning(msg, id='caches.W002'),
+ ])
+
+ def test_cache_path_contains_media_static_setting(self):
+ root = pathlib.Path.cwd()
+ for setting in ('MEDIA_ROOT', 'STATIC_ROOT', 'STATICFILES_DIRS'):
+ settings = self.get_settings(setting, root, root / 'other')
+ with self.subTest(setting=setting), self.settings(**settings):
+ msg = self.warning_message % ('contains', setting)
+ self.assertEqual(check_cache_location_not_exposed(None), [
+ Warning(msg, id='caches.W002'),
+ ])
+
+ def test_cache_path_not_conflict(self):
+ root = pathlib.Path.cwd()
+ for setting in ('MEDIA_ROOT', 'STATIC_ROOT', 'STATICFILES_DIRS'):
+ settings = self.get_settings(setting, root / 'cache', root / 'other')
+ with self.subTest(setting=setting), self.settings(**settings):
+ self.assertEqual(check_cache_location_not_exposed(None), [])