Django caching useless with CSRF protection?

Hi, my site has a searchbox on each page, so I placed a {% csrf_token %} tag in the base template. Activating a per-site cache (or a per-view cache with the @cache_page and @csrf_protect decorators) does not affect the site’s performance at all, as the pages change with each request, so nothing is ever cached. Am I right, or is there some magical way around this (other than template fragment caching)?

I believe you are correct.

That doesn’t mean there aren’t possible work-arounds. I can think of two right off-hand.

  • URL-encode the contents of the searchbox, and submit it as a GET instead of a POST
  • If the view invoked by the searchbox is truly safe, mark that view as csrf_exempt
1 Like

Thank you! I am not confident that my search view (s. below) is truly safe, and am sufficiently terrified by CSRF prevention recommendations like this to avoid workarounds. I have tested template fragment caching successfully, although this approach does not leverage browser caching.

def search(request):
	if request.method == 'POST':
		form = SearchForm(request.POST)
		if form.is_valid():
			results = Page.objects.filter(title__icontains=form.cleaned_data['page_title'], content__icontains=form.cleaned_data['page_content'], author__last_name__icontains=form.cleaned_data['page_author']).order_by('title')
			return render(request, 'results.html', { 'results' : results,} )
	else:
		form = SearchForm()
	return render(request, 'form.html',
		{
			'target' : reverse(search),
			'form' : form,
		})

PS. You are one of the most friendly and competent people I met in any technical forum. Thanks for your time and patience.

A search would be “safe” in this context if there is no data being written. (No objects being created or updated, and no “user status” change such as a login/logout.)

I would have the opinion that this view would be considered safe.

1 Like

Hi, I faced the same issue. According to the CSRF docs, you can use:

from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_protect


@cache_page(60 * 15)
@csrf_protect
def my_view(request): ...

But this is kind of useless, since this just adds the Vary: Cookie header to the response, witch means the cache stores a separate version for each user, where the only real difference is the CSRF token—whether in a cookie or the masked token from {% csrf_token %} in the template…

Another way around is to modify a bit how the cache_page decorator works by calling the get_token() method after retrieving the cached response. This of course only works to set the Set-Cookie http header on the response but it won’t work for the {% csrf_token %} on a template. In witch case you are forced to use AJAX, as mentioned in the docs.

from django.middleware.cache import CacheMiddleware
from django.middleware.csrf import get_token
from django.utils.decorators import decorator_from_middleware_with_args


class CSRFCacheMiddleware(CacheMiddleware):

    def process_request(self, request):
        response = super().process_request(request)
        get_token(request)
        return response


def secure_cache_page(timeout, *, cache=None, key_prefix=None):
    return decorator_from_middleware_with_args(CSRFCacheMiddleware)(
        page_timeout=timeout,
        cache_alias=cache,
        key_prefix=key_prefix,
    )