public
Last active

Webpy CSRF protection (request for comments)

  • Download Gist
README
1 2 3 4 5 6 7 8
A simple trick against CSRF for web.py (webpy.org)
* At the GET() template, you add a hidden field called csrf_token with value "$csrf_token()"
* The POST() should have the @csrf_protected decorator
That's it.
 
Request for comments:
* Is this secure? Can you see any holes?
* Is there anything in [or for] web.py that does this? Am I reinvevting the wheel here?
index.html
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$def with(args)
<!doctype html>
<html lang="en">
<head>
<title>$args['title']</title>
</head>
<body>
<form method=post action="">
<input type=hidden name=csrf_token value="$csrf_token()">
<label for=message>Message:</label>
<input id=message name=message value="$args['message']">
<input type=submit value="Update">
</form>
$if args['veteran']:
If you <a href="javascript:history.back()">go back</a> and try to update the previous page,<br>
you'll be detected as a CSRF attack. Good luck :)
</body>
</html>
webpy-csrf-example.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
import web
urls = ("/", "testme")
 
app = web.application(urls, globals())
 
# Session/debug tweak from http://webpy.org/cookbook/session_with_reloader
if web.config.get('_session') is None:
session = web.session.Session(app, web.session.DiskStore('sessions'))
web.config._session = session
else:
session = web.config._session
 
def csrf_token():
"""Should be called from the form page's template:
<form method=post action="">
<input type=hidden name=csrf_token value="$csrf_token()">
...
</form>"""
if not session.has_key('csrf_token'):
from uuid import uuid4
session.csrf_token=uuid4().hex
return session.csrf_token
 
def csrf_protected(f):
"""Usage:
@csrf_protected
def POST(self):
..."""
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>.')
return f(*args,**kwargs)
return decorated
 
# Note: in order to let templates use csrf_token, you need to add it to render's globals
render = web.template.render('.',globals={'csrf_token':csrf_token})
 
class testme:
def GET(self):
return render.index({ # I know it's not customary to pass a dict, but it's neater IMHO
'title':'WebPy CSRF example',
'veteran':False,
'message':session.get('message')})
@csrf_protected
def POST(self):
session.message = web.input().message
session.status = 'Updated message'
return render.index({ # I know it's not customary to pass a dict, but it's neater IMHO
'title':'Message updated',
'veteran':True,
'message':session.get('message')})
 
if __name__ == "__main__":
app.run()

I think it be more elegant if we add a new Input field class for CSRF and let that validate on form.validates() instead of using decorators. Other option it to add an application processor that inspects all POST data and verifies it against CSRF attacks.

And it will be nice, if this can be done using cookies instead of sessions.

Very nice. and what if i open two forms in two tabs and then try to submit both? maybe bind csrf tokens in the session to URLs? I also agree with Anand that using cookies (at least when they are available) will be nicer. That will give you another advantage - you don't need to print the token as part of the form, you set it using a header, which means that you don't have to modify your forms at all, you can apply that using a decorator too (or maybe even a single decorator on the class).

Sorry it took me so long (I'm pretty much offline lately):

@intellectronica - You're right. I sorta circumvent this now by adding a more tolerant error message (that also includes a link back to the form). Doing it "the right way" would require a lot more coding, and I'm too busy with my actual webpy app at the moment :(

@anandology - I've added a pull request to a cookbook example: thedod/webpy.github.com@a862368b01ce359e77d2 (hope it's useful). Both remarks by you and @intellectronica are mentioned in the commit message. I hope someone else writes a more complete solution sooner or later, and all I need to do is incorporate it into my apps :)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.