diff options
| author | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-06-09 08:46:36 -0400 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-06-09 10:56:46 -0400 |
| commit | 142b881cecaddc334cabec139e701c0e4b9798da (patch) | |
| tree | d1b86655cd376a37ae696f0125cbfd3e40f1192f | |
| parent | 71af23d1f4678c4bf052e051d72e8928e1697de3 (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.py | 6 | ||||
| -rw-r--r-- | tests/cache/tests.py | 27 |
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 |
