How to protect forms from CSRF attacks
Problem
How to make sure a POST form submission genuinely originates from a form created by the application, and is not a Cross-Site Request Forgery.
Solution
We keep a unique csrf_token that is rendered as a hidden field inside post forms and can not be guessed by CSRF attackers. This token gets checked during POST methods.
We need 4 things:
-
A
csrf_token()
function - to use inside form templates. It either returns the existingsession.csrf_token
or generates a new one. -
A
@csrf_protected
decorator forPOST()
methods. It popssession.csrf_token
and compares it with thecsrf_token
input we expect to get from a genuine form (see<input type="hidden" ...>
below. Whether the test succeeds or fails, this will make sure that next timecsrf_token()
is called (most probably - from inside a form’s template), a new token will be generated. -
Make
csrf_token()
available to templates by adding it to the globals of ourrender
object. -
Add
<input type="hidden" name="csrf_token" value="$csrf_token()"/>
to the forms in the templates.
We define csrf_token()
like this:
def csrf_token():
if not session.has_key('csrf_token'):
from uuid import uuid4
session.csrf_token=uuid4().hex
return session.csrf_token
The @csrf_protected
decorator is defined like this:
def csrf_protected(f):
def decorated(*args,**kwargs):
inp = web.input()
if not (inp.has_key('csrf_token') and inp.csrf_token==session.pop('csrf_token',None)):
raise web.HTTPError(
"400 Bad request",
{'content-type':'text/html'},
"""Cross-site request forgery (CSRF) attempt (or stale browser form).
<a href="">Back to the form</a>."""') # Provide a link back to the form
return f(*args,**kwargs)
return decorated
In order to make csrf_token() available to templates, we need to add it to the globals of the render
object like this:
render = web.template.render('templates',globals={'csrf_token':csrf_token})
A template that renders a POST form (called - say - myform.html
) would look like:
<form method=post action="">
<input type="hidden" name="csrf_token" value="$csrf_token()"/>
# ... form fields ...
</form>
If we’re using a Form
object from web.form
called form, our myform.html
template would look like:
<form method=post action="">
<input type="hidden" name="csrf_token" value="$csrf_token()"/>
$:form.render()
</form>
The form page’s object would then look like:
class myformpage:
def GET(self):
return render.myform(...)
@csrf_protected # Verify this is not CSRF, or fail
def POST(self):
# If we're here - this is not a CSRF attack
A simple working demo is availale here.