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