NB: This is the third post in a series of posts on web application security.
The quintessential example of a CSRF (sometimes pronounced “sea-surf”) is a bank that naively does transfers over a
GET request without any other security:
Ignoring how many other bad things are going on here, there’s no validation that the request is coming from a site. I could include an image tag somewhere with that URL and every time someone visited the site, I’d get another hundred grand.
Django, and most frameworks, have built-in CSRF protection that uses the concept of a nonce, or one-time-use number. These are submitted with a form (over
POST, hopefully) and compared to a number on the server-side. If the number doesn’t match, the request is prohibited.
Update: Because of an insightful comment on HN, I want to make it clear that using
POST does not guarantee safety from CSRF. Someone can use an
<iframe> and execute a
POST request. But it does two things:
- It reduces the surface area of attack. There are a lot more places I can stick an
<img>tag that can trigger a
GETrequest on high-traffic parts of the web than there are places I can stick some code to create an
iframeand do a
- It’s a better practice to do make any change a
POSTbecause all the actors, from the browser/user-agent, to proxies, to load-balancers, to web servers, understand that the
POSTverb means something is changing and treat it differently than most
Whatever your framework provides for this, use it.
Testing CSRF Protection
Testing CSRF protection is tricky, especially in Django. The built-in test client ignores CSRF protections, so you may break parts of your site without knowing.
Using something like Selenium to drive tests through form submissions in actual browsers can help guarantee that you both have CSRF protection in place and that you’re providing all the credentials and nonces correctly to the browser.
(And yes, we’ve stumbled over and broken that before. It took us a few days to track down all the forms we broke.)