-
-
Save thedod/857297 to your computer and use it in GitHub Desktop.
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? |
$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> |
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() |
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 :)
@anandology I have implemented your first suggestion (adding a CSRF input field). And submitted a pull request (Pull Request #290 ) Unfortunately it uses sessions, perhaps someone could change that?
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.