From 2115d4eaee15107f5cd290d7cfcc5ffe3ad43661 Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Thu, 23 Apr 2026 14:53:28 +0200
Subject: [PATCH] [5.2.x] Fixed CVE-2026-6907 -- Prevented caching of requests
 when Vary header contains an asterisk.

Thank you Ahmad Sadeddin for the report and Jacob Walls for the review.

Backport of c79bdfc1351ef2a2ad95df36241a74c736ef20a1 from main.
---
 django/middleware/cache.py |  4 ++++
 docs/releases/5.2.14.txt   | 10 ++++++++++
 tests/cache/tests.py       | 25 +++++++++++++++++++++++++
 3 files changed, 39 insertions(+)

diff --git a/django/middleware/cache.py b/django/middleware/cache.py
index df26def6b414..880ceaf7c15a 100644
--- a/django/middleware/cache.py
+++ b/django/middleware/cache.py
@@ -104,6 +104,10 @@ def process_response(self, request, response):
         if "private" in response.get("Cache-Control", ()):
             return response
 
+        # Don't cache responses when the Vary header contains '*'.
+        if has_vary_header(response, "*"):
+            return response
+
         # Page timeout takes precedence over the "max-age" and the default
         # cache timeout.
         timeout = self.page_timeout
diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index 2636a7d6ce09..306a6ac3ffca 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -2505,6 +2505,18 @@ def hello_world_view(request, value):
     return HttpResponse("Hello World %s" % value)
 
 
+def hello_world_view_patch_vary_headers_asterisk(request, value):
+    response = HttpResponse("Hello World %s" % value)
+    patch_vary_headers(response, ("*",))
+    return response
+
+
+def hello_world_view_vary_headers_includes_asterisk(request, value):
+    response = HttpResponse("Hello World %s" % value)
+    response["Vary"] = "Cookie, *, Pony"
+    return response
+
+
 def csrf_view(request):
     return HttpResponse(csrf(request)["csrf_token"])
 
@@ -2733,6 +2745,19 @@ def test_cached_control_private_not_cached(self):
         response = view_with_private_cache(request, "2")
         self.assertEqual(response.content, b"Hello World 2")
 
+    def test_vary_asterisk_not_cached(self):
+        views_with_cache = (
+            cache_page(3)(hello_world_view_patch_vary_headers_asterisk),
+            cache_page(3)(hello_world_view_vary_headers_includes_asterisk),
+        )
+        for view in views_with_cache:
+            with self.subTest(view=view):
+                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 2")
+
     def test_sensitive_cookie_not_cached(self):
         """
         Django must prevent caching of responses that set a user-specific (and
