CSRF: Cross-Site Request Forgeries - Basic Security Part 3

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:

http://badbank.com/transfer?from=act1&to=act2&amt=100000.00

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 GET request on high-traffic parts of the web than there are places I can stick some code to create an iframe and do a POST.
  • It’s a better practice to do make any change a POST because all the actors, from the browser/user-agent, to proxies, to load-balancers, to web servers, understand that the POST verb means something is changing and treat it differently than most GET requests.

Whatever your framework provides for this, use it.

If you’re using Django, you may also want to look at django-session-csrf, which provides a bit more security than the default implementation, which uses cookies. It stores the actual CSRF token (nonce) in the session, instead of in its own cookie. This provides better protection against compromised apps on different subdomains and MITM attacks that can alter cookie values.

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.)