Flask-WTF:缺少CSRF令牌

时间:2016-08-31 22:52:45

标签: python session flask wtforms flask-wtforms

看起来像一个简单的错误 - 由于“CSRF令牌丢失”错误而无法通过的表单提交 - 已经变成了一天的头发拉动。我已经阅读了与Flask或Flask-WTF相关的每篇SO文章以及缺少CSRF令牌,似乎没有任何帮助。

以下是详细信息:

关注Martijin's guidelines之前的问题:

  

Flask-WTF CSRF基础设施在以下情况下拒绝令牌:

     

1)令牌丢失。不是这里的情况,您可以在表单中看到令牌。

令牌肯定存在于我的表单中,并成功发布了

  

2)它太旧了(默认过期时间设置为3600秒或一小时)。   在窗体上设置TIME_LIMIT属性以覆盖它。可能不是   案例在这里。

对我来说还可以 - 令牌完全在默认的到期时间内

  

3)如果在当前会话中找不到'csrf_token'键。您可以   显然会看到会话令牌,所以也是如此。

在我的情况下,会话['csrf_token']已正确设置并由Flask查看

  

4)如果HMAC签名不匹配;签名是基于   在'csrf_token'键下的会话中设置的随机值,   服务器端密码,以及令牌中的到期时间戳。

这是我的问题。提交的表单的CSRF和会话CSRF之间的HMAC比较失败。但我不知道如何解决它。我一直非常绝望(与其他提问者一样)深入研究Flask-WTF代码并设置调试消息以找出正在发生的事情。我可以说,它的工作原理如下:

1)“form.py”中的generate_csrf_token()(Flask-WTF)想要生成CSRF令牌。所以它叫:

2)“csrf.py”中的generate_csrf()。如果不存在,该函数会生成一个新会话['csrf_token']。 在我的情况下,这总是发生 - 虽然其他会话变量似乎在请求之间保持不变,但我的调试显示我从不在我的会话开始时有一个'csrf_token'一个要求。这是正常的吗?

3)当我在模板上渲染隐藏字段时,返回生成的标记并可能合并到表单变量中。 (再次,调试显示此令牌出现在表单中并正确提交和接收)

4)接下来,提交表格。

5)现在,调用csrf.py中的validate_csrf。但由于发生了另一个请求,并且generate_csrf()已生成新的会话CSRF令牌,因此两个令牌(在会话中和从表单中)的两个时间戳将不匹配。由于CSRF部分由到期日组成,因此验证失败。

我怀疑问题出现在步骤#2中,其中为每个请求生成了一个新令牌。但我不知道为什么会话中的其他变量会从请求到请求持续存在,而不是“csrf_token”。

SECRET_KEY或WTF_CSRF_SECRET_KEY也没有出现任何奇怪现象(它们已正确设置)。

有人有什么想法吗?

2 个答案:

答案 0 :(得分:4)

我明白了。它似乎是一个cookie /会话限制(可能超出Flask的控制范围),并且在达到限制时会静默丢弃会话变量(这看起来更像是一个错误)。

以下是一个例子:

<强>模板/ hello.html的

<p>{{ message|safe }}</p>
<form name="loginform" method="POST">
  {{ form.hidden_tag() }}
  {{ form.submit_button() }}
</form>

<强> myapp.py

from flask import Flask, make_response, render_template, session
from flask_restful import Resource, Api
from flask_wtf import csrf, Form
from wtforms import SubmitField

app = Flask(__name__)
app.secret_key = '5accdb11b2c10a78d7c92c5fa102ea77fcd50c2058b00f6e'
api = Api(app)

num_elements_to_generate = 500

class HelloForm(Form):
    submit_button = SubmitField('Submit This Form')

class Hello(Resource):
    def check_session(self):
        if session.get('big'):
            message = "session['big'] contains {} elements<br>".format(len(session['big']))
        else:
            message = "There is no session['big'] set<br>"
        message += "session['secret'] is {}<br>".format(session.get('secret'))
        message += "session['csrf_token'] is {}<br>".format(session.get('csrf_token'))
        return message

    def get(self):
        myform = HelloForm()
        session['big'] = list(range(num_elements_to_generate))
        session['secret'] = "A secret phrase!"
        csrf.generate_csrf()
        message = self.check_session()
        return make_response(render_template("hello.html", message=message, form=myform), 200, {'Content-Type': 'text/html'})

    def post(self):
        csrf.generate_csrf()
        message = self.check_session()
        return make_response("<p>This is the POST result page</p>" + message, 200, {'Content-Type': 'text/html'})

api.add_resource(Hello, '/')

if __name__ == '__main__':
    app.run(debug=True)

num_elements_to_generate设置为500的情况下运行此操作,您将获得以下内容:

session['big'] contains 500 elements
session['secret'] is 'A secret phrase!'
session['csrf_token'] is a6acb57eb6e62876a9b1e808aa1302d40b44b945

和“提交此表单”按钮。点击按钮,您将获得:

This is the POST result page
session['big'] contains 500 elements
session['secret'] is 'A secret phrase!'
session['csrf_token'] is a6acb57eb6e62876a9b1e808aa1302d40b44b945

一切都很好。但现在将num_elements_to_generate更改为3000,清除您的Cookie,重新运行该应用并访问该页面。你会得到类似的东西:

session['big'] contains 3000 elements
session['secret'] is 'A secret phrase!'
session['csrf_token'] is 709b239857fd68a4649deb864868897f0dc0a8fd

和“提交此表单”按钮。点击按钮,这次你会得到:

This is the POST result page
There is no session['big'] set
session['secret'] is 'None'
session['csrf_token'] is 13553dce0fbe938cc958a3653b85f98722525465

会话变量中存储的3,000位数字太多,因此会话变量不会在请求之间保持不变。有趣的是,它们存在于第一页的会话中(无论您生成多少元素),但它们无法生存到下一个请求。和Flask-WTF一样,因为在发布表单时会话中没有看到csrf_token,所以会生成一个新表单。如果这是表单验证步骤,则CSRF验证将失败。

这似乎是一个已知的Flask(或Werkzeug)错误,with a pull request here。我不确定为什么Flask没有在这里产生警告 - 除非它在某种程度上在技术上是不可行的,这是一个意想不到的令人不快的惊喜,当cookie太大时它无声地保持会话变量。

答案 1 :(得分:0)

不要经历上面提到的漫长过程,只需将以下 jinja 代码 {{ form.csrf_token }} 添加到表单的 html 端,这应该处理“CSRF 令牌丢失”错误。所以在 HTML 方面,它看起来像这样:

<form action="{{url_for('signup')}}" method="POST">
    {{ form.csrf_token }}

<fieldset class="name">
    {{ form.name.label}}
    {{ form.name(placeholder='John Doe')}}
</fieldset>
.
.
.
{{ form.submit()}}