summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2026-06-09 08:46:36 -0400
committerJacob Walls <jacobtylerwalls@gmail.com>2026-06-09 10:56:46 -0400
commit142b881cecaddc334cabec139e701c0e4b9798da (patch)
treed1b86655cd376a37ae696f0125cbfd3e40f1192f
parent71af23d1f4678c4bf052e051d72e8928e1697de3 (diff)
Refs #36560, CVE-2026-35193 -- Replaced substring check on cache-control directives in UpdateCacheMiddleware.
Avoid false positives from hypothetical extension directives that could be superstrings of the ones we are checking.
-rw-r--r--django/middleware/cache.py6
-rw-r--r--tests/cache/tests.py27
2 files changed, 31 insertions, 2 deletions
diff --git a/django/middleware/cache.py b/django/middleware/cache.py
index 8ac1178b12..60c219064a 100644
--- a/django/middleware/cache.py
+++ b/django/middleware/cache.py
@@ -56,6 +56,7 @@ from django.utils.cache import (
learn_cache_key,
patch_response_headers,
patch_vary_headers,
+ split_header_value,
)
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import parse_http_date_safe
@@ -106,8 +107,9 @@ class UpdateCacheMiddleware(MiddlewareMixin):
# Don't cache responses when the Cache-Control header is set to
# private, no-cache, or no-store.
cache_control = response.get("Cache-Control", "").lower()
+ cache_control_parts = list(split_header_value(cache_control))
if cache_control and any(
- directive in cache_control
+ directive in cache_control_parts
for directive in (
"private",
"no-cache",
@@ -137,7 +139,7 @@ class UpdateCacheMiddleware(MiddlewareMixin):
# header, unless allowed by "public" per RFC 9111, Section 3.5. No
# exceptions are made for "s-maxage" and "must-revalidate" since these
# are not currently implemented by Django.
- if request.headers.get("Authorization") and "public" not in cache_control:
+ if request.headers.get("Authorization") and "public" not in cache_control_parts:
patch_vary_headers(response, ("Authorization",))
if timeout and response.status_code == 200:
cache_key = learn_cache_key(
diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index 92641240b1..4bc3e1e9ce 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -3026,6 +3026,23 @@ class CacheMiddlewareTest(SimpleTestCase):
response = view(request, "2")
self.assertEqual(response.content, b"Hello World 2")
+ def test_cache_control_not_cached_superstring(self):
+ """
+ "myprivate", a hypothetical extension directive, is not confused for
+ "private".
+ """
+
+ @cache_page(3)
+ @cache_control(myprivate=True)
+ def view(request, value):
+ return HttpResponse(f"Hello World {value}")
+
+ request = self.factory.get("/view/")
+ response = view(request, "1")
+ self.assertEqual(response.content, b"Hello World 1")
+ response = view(request, "2")
+ self.assertEqual(response.content, b"Hello World 1")
+
def test_vary_asterisk_not_cached(self):
views_with_cache = (
cache_page(3)(hello_world_view_patch_vary_headers_asterisk),
@@ -3064,6 +3081,16 @@ class CacheMiddlewareTest(SimpleTestCase):
response = view_with_cache(request, "1")
self.assertIs(has_vary_header(response, "Authorization"), False)
+ def test_authorization_header_exception_superstring(self):
+ """
+ "nopublic", a hypothetical extension directive, is not confused for
+ "public".
+ """
+ view_with_cache = cache_page(3)(cache_control(no_public=True)(hello_world_view))
+ request = self.factory.get("/view/", headers={"Authorization": "token"})
+ response = view_with_cache(request, "1")
+ self.assertIs(has_vary_header(response, "Authorization"), True)
+
def test_sensitive_cookie_not_cached(self):
"""
Django must prevent caching of responses that set a user-specific (and