The django.contrib.csrf package protects against Cross-Site Request Forgery (CSRF). CSRF, also known as “session riding,” is a Web site security exploit. It happens when a malicious Web site tricks a user into unknowingly loading a URL from a site at which that user is already authenticated, hence taking advantage of the user’s authenticated status. This can be a bit tricky to understand at first, so we walk through two examples in this section.
A Simple CSRF Example – Suppose you’re logged in to a webmail account at example.com. This webmail site has a Log Out button that points to the URL example.com/logout — that is, the only action you need to take in order to log out is to visit the page example.com/logout. A malicious site can coerce you to visit the URL example.com/logout by including that URL as a hidden <iframe> on its own (malicious) page. Thus, if you’re logged in to the example.com webmail account and visit the malicious page that has an <iframe> to example.com/logout, the act of visiting the malicious page will log you out from example.com.
Clearly, being logged out of a webmail site against your will is not a terrifying breach of security, but
this same type of exploit can happen to any site that “trusts” users, such as an online banking site or an e-commerce site.
A More Complex CSRF Example – In the previous example, example.com was partially at fault because it allowed a state change (i.e., logging the user out) to be requested via the HTTP GET method. It’s much better practice to require an HTTP POST for any request that changes state on the server. But even Web sites that require POST for state-changing actions are vulnerable to CSRF.
Suppose example.com has upgraded its Log Out functionality so that it’s a <form> button that is requested via POST to the URL example.com/logout. Furthermore, the logout <form> includes this hidden field:
<input type=”hidden” name=”confirm” value=”true” />
This ensures that a simple POST to the URL example.com/logout won’t log a user out; in order for a user to log out, the user must request example.com/logout via POST and send the confirm POST variable with a value of ‘true’. Well, despite the extra security, this arrangement can still be exploited by CSRF — the malicious page just needs to do a little more work. Attackers can create an entire form targeting your site, hide it in an invisible <iframe>, and then use JavaScript to submit that form automatically.
Preventing CSRF – How, then, can your site protect itself from this exploit? The first step is to make sure all GET requests are free of side effects. That way, if a malicious site includes one of your pages as an <iframe>, it won’t have a negative effect.
That leaves POST requests. The second step is to give each POST <form> a hidden field whose value is secret and is generated from the user’s session ID. Then, when processing the form on the server side, check for that secret field and raise an error if it doesn’t validate. This is exactly what Django’s CSRF prevention layer does, as explained in the sections that follow.
Using the CSRF Middleware – The django.contrib.csrf package contains only one module: middleware.py. This module contains a Django middleware class, CsrfMiddleware, which implements the CSRF protection.
To activate this CSRF protection, add ‘django.contrib.csrf.middleware.CsrfMiddleware’ to the MIDDLEWARE_CLASSES setting in your settings file. This middleware needs to process the response after SessionMiddleware, so CsrfMiddleware must appear before SessionMiddleware in the list (because the response middleware is processed last-to-first). Also, it must process the response before the response gets compressed or otherwise mangled, so CsrfMiddleware must come after GZipMiddleware. Once you’ve added that to your MIDDLEWARE_CLASSES setting, you’re done. See the section “Order of MIDDLEWARE_CLASSES” in Chapter 13 for more explanation. In case you’re interested, here’s how CsrfMiddleware works. It does these two things:
- It modifies outgoing requests by adding a hidden form field to all POST forms, with the name csrfmiddlewaretoken and a value that is a hash of the session ID plus a secret key. The middleware does not modify the response if there’s no session ID set, so the performance penalty is negligible for requests that don’t use sessions.
- On all incoming POST requests that have the session cookie set, it checks that csrfmiddlewaretoken is present and correct. If it isn’t, the user will get a 403 HTTP The content of the 403 error page is the message “Cross Site Request Forgery detected. Request aborted.”
This ensures that only forms originating from your Web site can be used to POST data back. This middleware deliberately targets only HTTP POST requests (and the corresponding POST forms). As we explained, GET requests ought never to have side effects; it’s your own responsibility to ensure this.
POST requests not accompanied by a session cookie are not protected, but they don’t need to be protected, because a malicious Web site could make these kind of requests anyway. To avoid altering non-HTML requests, the middleware checks the response’s Content-Type header before modifying it. Only pages that are served as text/html or application/xml+xhtml are modified.
Limitations of the CSRF Middleware – CsrfMiddleware requires Django’s session framework to work. (See Chapter 12 for more on sessions.) If you’re using a custom session or authentication framework that manually manages session cookies, this middleware will not help you. If your application creates HTML pages and forms in some unusual way (e.g., if it sends fragments of HTML in JavaScript document.write statements), you might bypass the filter that adds the hidden field to the form. In this case, the form submission will always fail. (This happens because CsrfMiddleware uses a regular expression to add the csrfmiddlewaretoken field to your HTML before the page is sent to the client, and the regular expression sometimes cannot handle wacky HTML.) If you suspect this might be happening, just view the source in your Web browser to see whether csrfmiddlewaretoken was inserted into your <form>.