Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Origin: backport, https://github.com/django/django/commit/1170f285ddd6a94a65f911a27788ba49ca08c0b0
Date: Sat Mar 29 14:48:15 2014 +0100
Subject: Prevent leaking the CSRF token through caching.
    
Django includes both a caching framework and a system for preventing
cross-site request forgery (CSRF) attacks. The CSRF-protection system
is based on a random nonce sent to the client in a cookie which must
be sent by the client on future requests, and in forms a hidden value
which must be submitted back with the form.

The caching framework includes an option to cache responses to
anonymous (i.e., unauthenticated) clients.

When the first anonymous request to a given page was by a client which
did not have a CSRF cookie, the cache framework will also cache the
CSRF cookie, and serve the same nonce to other anonymous clients who
do not have a CSRF cookie. This allows an attacker to obtain a valid
CSRF cookie value and perform attacks which bypass the check for the
cookie.

To remedy this, the caching framework will no longer cache such
responses. The heuristic for this will be:

1. If the incoming request did not submit any cookies, and

2. The response did send one or more cookies, and

3. The ``Vary: Cookie`` header is set on the response, then the
   response will not be cached.

--- a/django/middleware/cache.py
+++ b/django/middleware/cache.py
@@ -50,7 +50,8 @@
 
 from django.conf import settings
 from django.core.cache import cache
-from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age
+from django.utils.cache import (get_cache_key, get_max_age, has_vary_header,
+    learn_cache_key, patch_response_headers)
 
 class UpdateCacheMiddleware(object):
     """
@@ -77,8 +78,15 @@
             # HTTPMiddleware, which throws the body of a HEAD-request
             # away before this middleware gets a chance to cache it.
             return response
+
         if not response.status_code == 200:
             return response
+
+        # Don't cache responses that set a user-specific (and maybe security
+        # sensitive) cookie in response to a cookie-less request.
+        if not request.COOKIES and response.cookies and has_vary_header(response, 'Cookie'):
+            return response
+
         # Try to get the timeout from the "max-age" section of the "Cache-
         # Control" header before reverting to using the default cache_timeout
         # length.
--- a/django/utils/cache.py
+++ b/django/utils/cache.py
@@ -134,6 +134,16 @@
                           if newheader.lower() not in existing_headers]
     response['Vary'] = ', '.join(vary_headers + additional_headers)
 
+def has_vary_header(response, header_query):
+    """
+    Checks to see if the response has a given header name in its Vary header.
+    """
+    if not response.has_header('Vary'):
+        return False
+    vary_headers = cc_delim_re.split(response['Vary'])
+    existing_headers = set([header.lower() for header in vary_headers])
+    return header_query.lower() in existing_headers
+
 def _i18n_cache_key_suffix(request, cache_key):
     """If enabled, returns the cache key ending with a locale."""
     if settings.USE_I18N:
