User-friendly Django CSRF Protection

The fact that Django comes with CSRF protection is extremely nice. Unless, of course, your hobby is exploiting websites, in which case get off my site. For the rest of us, thwart those baddies by using Django's CsrfViewMiddleware and the csrf_token tag. But when you do, keep in mind what that changes on your webpages. The csrf_token tag sets a cookie. So, that begs the question:

What about legitimate users that have their cookies disabled?

User-Unfriendliness

Sure, it's a fringe case, but let's address it, because it's easy. If Django can't set that cookie when the user submits the form, by default the user will see a 403 Forbidden error page that looks something like this:

Forbidden (403)

CSRF verification failed. Request aborted.

More information is available with DEBUG=True.

Yuck. For all intents and purposes, you just did the webdeveloper's equivalent of backhanding one of your unsuspecting users. It was probably someone's grandmother on a locked-down public library computer who now thinks she accidentally caused the local police to be notified. Are you proud of yourself?

One (somewhat) easy way out

Django, like usual, has your back. You can add something like this to settings.py:

CSRF_FAILURE_VIEW = 'path.to.friendly_csrf_failure_view'

Then create a view method with the name and path you supplied in that variable. Django will use your view instead of the sinister-looking 403 Forbidden page that worries grandmothers. Instead you can send them to a calming sky-blue page with a big yellow smiley face that asks if maybe they've disabled cookies. Thanks Django for providing a way out.

Even more friendly

But hang on, why wait until after grandma submits the form to lecture about cookies? We can do better, and we don't even need Django hooks for it. All we care about is that users with cookies disabled don't get slapped with the Forbidden error. We don't need to override the error page, we just need to test whether cookies work when the user first visits our form page. Then we can display a message to the user right then and there. Plus, we can keep the user from filling in the form before they've enabled cookies. This sounds like a job for JavaScript.

Here's an example of what to do using JQuery combined with jquery.cookie.js.

With JQuery and jquery.cookie.js included on the page, add something like this code snippet to your form page:

<!-- Replace any text in yellow for use on your site -->
<script>
$(function() {
    // Try creating a test cookie
    $.cookie('test_cookie', true);
    // Check if it can be read...
    if ($.cookie('test_cookie')) {
        // Our test cookie worked!
        // So, just delete it, and everything will work fine
        $.cookie('test_cookie', null);
    } else {
        // Cookies didn't work, so inject some
        // instructions to the user
        var html = '<p id="cookie_warning">' +
            'This form requires cookies, which are disabled on your browser.' +
            '<br>Please enable cookies by following ' +
            '<a target="_blank" href="/path/to/instructions">' +
            'these instructions</a>, and then <a href="/url/of/this/page">' +
            'click here</a> to use the form.</p>';
        var my_form = $("#my_form_id");
        my_form.prepend(html);
        // Now disable all the form elements
        my_form.find(':input:not(:disabled)').prop('disabled', true);
    }
});
</script>

And that's it. A few lines of code and your site is grandmother-worthy. If cookies are disabled, the user will never see a 403 Forbidden. Instead, the user will get instructions and the form won't be active until they follow them. The instructions open in a separate window, so the user doesn't leave your form page. For bonus friendliness, you can also do the following:

  • style the message using CSS so that it stands out
  • style the disabled form to "look" unusable (use the :disabled selector in CSS3)
  • add a <noscript> tag containing an informational cookie message, in case the user has disabled JavaScript

Posted 1/3/2013