Flask-WTF:缺少 CSRF 令牌
Flask-WTF: CSRF token missing
看似简单的错误 - 由于 "CSRF token missing" 错误而无法通过的表单提交 - 已经变成了一天的麻烦事。我已经阅读了所有与 Flask 或 Flask-WTF 相关的 SO 文章,并且缺少 CSRF 令牌,但似乎没有任何帮助。
详情如下:
关注 Martijin's guidelines 之前的问题:
The Flask-WTF CSRF infrastructure rejects a token if:
1) the token is missing. Not the case here, you can see the token in the form.
令牌肯定存在于我的表单中,并且POST已成功
2) it is too old (default expiration is set to 3600 seconds, or an hour).
Set the TIME_LIMIT attribute on forms to override this. Probably not the
case here.
我也可以 - 令牌在默认到期时间内很好
3) if no 'csrf_token' key is found in the current session. You can
apparently see the session token, so that's out too.
在我的例子中,session['csrf_token'] 已正确设置并被 Flask
看到
4) If the HMAC signature doesn't match; the signature is based on the
random value set in the session under the 'csrf_token' key, the
server-side secret, and the expiry timestamp in the 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 步,其中为每个请求生成了一个新令牌。但是我不知道为什么我的会话中的 other 变量在请求之间持续存在,而不是 "csrf_token".
SECRET_KEY 或 WTF_CSRF_SECRET_KEY 都没有异常(它们已正确设置)。
有人有什么想法吗?
我想通了。它似乎是一个 cookie/session 限制(这可能超出了 Flask 的控制范围)并且在达到限制时静默丢弃会话变量(这看起来更像是一个错误)。
这是一个例子:
templates/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
和一个 "Submit This Form" 按钮。点击按钮,您将获得:
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
和一个 "Submit This Form" 按钮。点击按钮,这次你会得到:
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 太大时它会默默地未能保留会话变量,这是一个意外和令人不快的惊喜。
无需经历上面提到的漫长过程,只需将以下 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()}}
看似简单的错误 - 由于 "CSRF token missing" 错误而无法通过的表单提交 - 已经变成了一天的麻烦事。我已经阅读了所有与 Flask 或 Flask-WTF 相关的 SO 文章,并且缺少 CSRF 令牌,但似乎没有任何帮助。
详情如下:
关注 Martijin's guidelines 之前的问题:
The Flask-WTF CSRF infrastructure rejects a token if:
1) the token is missing. Not the case here, you can see the token in the form.
令牌肯定存在于我的表单中,并且POST已成功
2) it is too old (default expiration is set to 3600 seconds, or an hour). Set the TIME_LIMIT attribute on forms to override this. Probably not the case here.
我也可以 - 令牌在默认到期时间内很好
3) if no 'csrf_token' key is found in the current session. You can apparently see the session token, so that's out too.
在我的例子中,session['csrf_token'] 已正确设置并被 Flask
看到4) If the HMAC signature doesn't match; the signature is based on the random value set in the session under the 'csrf_token' key, the server-side secret, and the expiry timestamp in the 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 步,其中为每个请求生成了一个新令牌。但是我不知道为什么我的会话中的 other 变量在请求之间持续存在,而不是 "csrf_token".
SECRET_KEY 或 WTF_CSRF_SECRET_KEY 都没有异常(它们已正确设置)。
有人有什么想法吗?
我想通了。它似乎是一个 cookie/session 限制(这可能超出了 Flask 的控制范围)并且在达到限制时静默丢弃会话变量(这看起来更像是一个错误)。
这是一个例子:
templates/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
和一个 "Submit This Form" 按钮。点击按钮,您将获得:
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
和一个 "Submit This Form" 按钮。点击按钮,这次你会得到:
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 太大时它会默默地未能保留会话变量,这是一个意外和令人不快的惊喜。
无需经历上面提到的漫长过程,只需将以下 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()}}