CSRF protection on AJAX authentication in Flask

2019-07-16 03:45发布

问题:

I'd like to AJAXify both a login and a signup form on a site. Up to now I've been using WTForms mainly for its built-in CSRF protetion, but for this project I didn't feel like it was worth it -- an extra layer of abstraction, and therefore frustration, for something that should be pretty simple.

So I came across this snippet on Flask's security section:

@app.before_request
def csrf_protect():
    if request.method == "POST":
        token = session.pop('_csrf_token', None)
        if not token or token != request.form.get('_csrf_token'):
        abort(403)

def generate_csrf_token():
    if '_csrf_token' not in session:
        session['_csrf_token'] = some_random_string()
    return session['_csrf_token']

app.jinja_env.globals['csrf_token'] = generate_csrf_token

I understand the thought process behind this code. In fact, it all makes perfect sense to me (I think). I can't see anything wrong with it.

But it doesn't work. The only thing I've changed about the code is replacing the pseudofunction some_random_string() with a call to os.urandom(24). Every request has 403'd so far because token and request.form.get('_csrf_token') are never the same. When I print them this becomes obvious -- usually they're different strings, but occasionally, and seemingly with no underlying reason, one or the other will be None or a truncated version of the output of os.urandom(24). Obviously something out of sync, but I'm not understanding what it is.

回答1:

You can get the convenience of flask-wtf without all the heaviness, and without rolling your own:

from flask_wtf.csrf import CsrfProtect

then on init, either:

CsrfProtect(app)

or:

csrf = CsrfProtect()

def create_app():
    app = Flask(__name__)
    csrf.init_app(app)

The token will then be available app-wide at any point, including via jinja2:

<form method="post" action="/">
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>

(via the docs)



回答2:

I think your problem is os.urandom function. The result of this function can contains symbols which not will parse properly in html. So when you insert csrf_token in html and don't do any escaping, you have the described problem.

How to fix. Try to escape csrf_token in html (see docs) or use another approach for generating csrf token. For example using uuid:

import uuid
...

def generate_random_string():
    return str(uuid.uuid4())
...