Skip to content

Instantly share code, notes, and snippets.

@thedod
Created March 6, 2011 13:51
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save thedod/857297 to your computer and use it in GitHub Desktop.
Save thedod/857297 to your computer and use it in GitHub Desktop.
Webpy CSRF protection (request for comments)
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()
@anandology
Copy link

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.

@intellectronica
Copy link

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

@thedod
Copy link
Author

thedod commented Mar 11, 2011

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

@SmileyJames2
Copy link

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment