如何编写路由以使用 Flask 接收内容安全策略报告而不会收到 400 错误请求错误 (flask_wtf.csrf.CSRFError)?
How can I write a route to receive Content Security Policy report with Flask without getting a 400 Bad Request error (flask_wtf.csrf.CSRFError)?
TL;DR:很抱歉 post。简而言之,我正在尝试调试 CSP report-uri
。如果我遗漏了重要信息,请告诉我。
CSP 实施:Flask-Talisman
需要设置的属性:content_security_policy_report_uri
似乎没有很多关于如何捕获此报告的信息
我在 Flask-Talisman
文档
中找不到任何具体内容
因为 Flask-Talisman
只设置 headers,包括 report-uri
,我想这无论如何都在扩展的范围之外
路线
我找到的所有资源都具有大致相同的功能:
https://www.merixstudio.com/blog/content-security-policy-flask-and-django-part-2/
http://csplite.com/csp260/
https://github.com/GoogleCloudPlatform/flask-talisman/issues/21
我找到的关于这条路线的唯一真正详细的解释如下(但它与 Flask-Talisman
无关)
来自https://www.merixstudio.com/blog/content-security-policy-flask-and-django-part-2/(这是我目前使用的)
# app/routes/report.py
import json
import pprint
from flask import request, make_response, Blueprint
...
bp = Blueprint("report", __name__, url_prefix="report")
...
@bp.route('/csp-violations', methods=['POST'])
def report():
"""Receive a post request containing csp-resport.
This is the report-uri. Print report to console and
return Response object.
:return: Flask Response object.
"""
pprint.pprint(json.loads(str(request.data, 'utf-8')))
response = make_response()
response.status_code = 200
return response
...
这条路线只收到 400 错误,我不知道如何实际调试它
127.0.0.1 - - [04/Nov/2021 14:29:09] "POST /report/csp-violations HTTP/1.1" 400 -
我已尝试使用 GET 请求,但可以看到我收到了一个空请求,这似乎意味着 CSP 报告未送达(来自 https://127.0.0.1:5000/report/csp-violations
的响应)
# app/routes/report.py
...
@bp.route('/csp-violations', methods=['GET', 'POST'])
def report():
...
b''
b''
127.0.0.1 - - [04/Nov/2021 18:03:52] "GET /report/csp_violations HTTP/1.1" 200 -
编辑:绝对没有收到任何东西
...
@bp.route("/csp_violations", methods=["GET", "POST"])
def report():
...
return str(request.args) # ImmutableMultiDict([ ])
不适用于 GET 或 POST 请求(仍然是 400)
...
@bp.route("/csp_violations", methods=["GET", "POST"])
def report():
content = request.get_json(force=True)
...
没有force=True
@bp.route("/csp_violations", methods=["GET", "POST"])
def report():
content = request.get_json() # json.decoder.JSONDecodeError
...
Chromium(在 Chrome、Brave 和 FireFox 上结果相同)
当我在 CTRL-SHIFT-I > 网络下的 Chromium 上查看时,我看到
Request URL: https://127.0.0.1:5000/report/csp_violations
Request Method: POST
Status Code: 400 BAD REQUEST
Remote Address: 127.0.0.1:5000
Referrer Policy: strict-origin-when-cross-origin
但显然负载确实存在于“请求负载”下的底部...
{
"document-uri": "https://127.0.0.1:5000/",
"referrer": "",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "default-src 'self' https://cdnjs.cloudflare.com https://cdn.cloudflare.com https://cdn.jsdelivr.net https://gravatar.com jquery.js; report-uri /report/csp-violations",
"disposition": "enforce",
"blocked-uri": "inline",
"line-number": 319,
"source-file": "https://127.0.0.1:5000/",
"status-code": 200,
"script-sample": ""
}
CSP 是否阻止了此请求?
看完 I set all requests to HTTPS with the help of
这修复了一些单元测试,但没有改变这条特定路线的 400 错误
# app/config.py
from environs import Env
from flask import Flask
env = Env()
env.read_env()
class Config:
"""Load environment."""
...
@property
def PREFERRED_URL_SCHEME(self) -> str: # noqa
return env.str("PREFERRED_URL_SCHEME", default="https")
...
# app/__init__.py
from app.config import Config
def create_app() -> Flask:
app = Flask(__name__)
app.config.from_object(Config())
...
return app
flask run --cert="$HOME/.openssl/cert.pem" --key="$HOME/.openssl/key.pem"
400 错误请求
据我所知,400 Bad Request 错误通常发生在空请求或表单中
...the issue is that Flask raises an HTTP error when it fails to find a key in
the args and form dictionaries. What Flask assumes by default is that if you
are asking for a particular key and it's not there then something got
left out of the request and the entire request is invalid.
99% of the time, this error is a key error caused by your requesting a key in
the request.form dictionary that does not exist. To debug it, run
print(request.form)
request.data
就我而言
通过尝试解决这个问题,我已经了解了 rabbit-hole 只是看到 400 错误的原因
我已经实现了以下
import traceback
from flask import Flask
...
app = Flask(__name__)
...
@app.errorhandler(400)
def internal_error(exception): # noqa
print("400 error caught")
print(traceback.format_exc())
并在我的配置中添加了以下内容
我没有检查过,但我认为这些值已经与 DEBUG
一起设置了
# app/config.py
from environs import Env
env = Env()
env.read_env()
class Config:
"""Load environment."""
...
@property
def TRAP_HTTP_EXCEPTIONS(self) -> bool: # noqa
"""Report traceback on error."""
return env.bool("TRAP_HTTP_EXCEPTIONS", default=self.DEBUG) # noqa
@property
def TRAP_BAD_REQUEST_ERRORS(self) -> bool: # noqa
"""Report 400 traceback on error."""
return env.bool("TRAP_BAD_REQUEST_ERRORS", default=self.DEBUG) # noqa
...
我仍然没有得到追溯。 werkzeug 唯一一次显示回溯是通过完全崩溃,例如语法错误
看来,因为我没有初始化这个请求,应用程序继续运行通过400代码,没问题
总结
我得出的主要结论是,出于某种原因,report-uri
无效,因为我可以看到有效负载存在,我只是没有收到它
我使用了相对路由,因为本地主机和远程主机的子域会有所不同。不过,看起来好像是在 Chromium 代码段中向完整的 URL 发出请求。
我会收到无效的 headers 吗?如果可以,我该如何调试?
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri
注意: report-uri
已弃用,但我认为大多数浏览器不支持 report-to
参数
编辑:
我已经从我的应用程序工厂中取出以下内容
(此代码呈现我的异常 - 我 post 在此处将其作为答案编辑,因此它可用
...
app = Flask(__name__)
config.init_app(app)
...
# exceptions.init_app(app)
...
return app
在这样做之后(在 FireFox 私有模式下),我做了另一个 POST(我已经删除了 GET 方法)
我可以看到响应 - 在这里我终于设法收集了一个 Werkzeug 回溯:
wtforms.validators.ValidationError: The CSRF token is missing.
研究如何验证来自浏览器的 csp-report
到目前为止我已经看过这个:
试试这段代码:
@app.route('/report-csp-violations', methods=['POST'])
def report():
content = request.get_json(force=True) # that's where the shoe pinches
print(json.dumps(content, indent=4, sort_keys=True))
response = make_response()
response.status_code = 204
return response
我认为 request.data
会尝试自动解析 JSON 数据,为了成功解析,它需要发送 application/json
MIME 类型。
但是违规报告是用 application/csp-report
MIME 类型发送的,因此 Flask 将其视为来自客户端的错误数据 -> 404 Bad Request。
.get_json(force=True)
表示忽略 mimetype 并始终尝试解析 JSON.
此外,我认为您不需要转换为 utf-8,因为根据 rfc4627“3. 编码”:
JSON 文本应使用 Unicode 编码。默认编码为 UTF-8。
目前我已将视图从 CRSFProtect
中排除。
我认为应该没问题,因为此视图没有 return 模板,所以我无法添加
<form method="post">
{{ form.csrf_token }}
</form>
下面的内容对我不起作用(不要认为它是有效的标记)
# app/routes/report.py
...
def report():
response = make_response()
response.headers["X-CSRFToken"] = csrf.generate_csrf()
...
return response
...
使用实例化的 CSRFProtect
对象...
# app/extensions.py
...
from flask_wtf.csrf import CSRFProtect
...
csrf_protect = CSRFProtect()
...
def init_app(app: Flask) -> None:
...
csrf_protect.init_app(app)
...
...
...装饰视图
# app/routes/report.py
from app.extensions import csrf_protect
...
@blueprint.route("/csp_violations", methods=["POST"])
@csrf_protect.exempt
def report():
....
127.0.0.1 - - [06/Nov/2021 21:30:46] "POST /report/csp_violations HTTP/1.1" 204 -
{
"csp-report": {
"blocked-uri": "inline",
"column-number": 8118,
"document-uri": "https://127.0.0.1:5000/",
"line-number": 3,
"original-policy": "default-src" ...
"referrer": "",
"source-file": ...
}
}
我有一个系统正在运行,我的错误被混淆了,所以这是我的错误。这不是我第一次遇到 CSRFProtect
的问题。
我已经用
解决了调试问题
# app/exceptions.py
...
def init_app(app: Flask) -> None:
...
def render_error(error: HTTPException) -> Tuple[str, int]:
# the below code is new
app.logger.error(error.description)
...
...
所以,如果我仍然收到此错误,这就是我现在看到的内容
[2021-11-06 21:23:56,054] ERROR in exceptions: The CSRF token is missing.
127.0.0.1 - - [06/Nov/2021 21:23:56] "POST /report/csp_violations HTTP/1.1" 400 -
TL;DR:很抱歉 post。简而言之,我正在尝试调试 CSP report-uri
。如果我遗漏了重要信息,请告诉我。
CSP 实施:Flask-Talisman
需要设置的属性:content_security_policy_report_uri
似乎没有很多关于如何捕获此报告的信息
我在 Flask-Talisman
文档
因为 Flask-Talisman
只设置 headers,包括 report-uri
,我想这无论如何都在扩展的范围之外
路线
我找到的所有资源都具有大致相同的功能:
https://www.merixstudio.com/blog/content-security-policy-flask-and-django-part-2/
http://csplite.com/csp260/
https://github.com/GoogleCloudPlatform/flask-talisman/issues/21
我找到的关于这条路线的唯一真正详细的解释如下(但它与 Flask-Talisman
无关)
来自https://www.merixstudio.com/blog/content-security-policy-flask-and-django-part-2/(这是我目前使用的)
# app/routes/report.py
import json
import pprint
from flask import request, make_response, Blueprint
...
bp = Blueprint("report", __name__, url_prefix="report")
...
@bp.route('/csp-violations', methods=['POST'])
def report():
"""Receive a post request containing csp-resport.
This is the report-uri. Print report to console and
return Response object.
:return: Flask Response object.
"""
pprint.pprint(json.loads(str(request.data, 'utf-8')))
response = make_response()
response.status_code = 200
return response
...
这条路线只收到 400 错误,我不知道如何实际调试它
127.0.0.1 - - [04/Nov/2021 14:29:09] "POST /report/csp-violations HTTP/1.1" 400 -
我已尝试使用 GET 请求,但可以看到我收到了一个空请求,这似乎意味着 CSP 报告未送达(来自 https://127.0.0.1:5000/report/csp-violations
的响应)
# app/routes/report.py
...
@bp.route('/csp-violations', methods=['GET', 'POST'])
def report():
...
b''
b''
127.0.0.1 - - [04/Nov/2021 18:03:52] "GET /report/csp_violations HTTP/1.1" 200 -
编辑:绝对没有收到任何东西
...
@bp.route("/csp_violations", methods=["GET", "POST"])
def report():
...
return str(request.args) # ImmutableMultiDict([ ])
不适用于 GET 或 POST 请求(仍然是 400)
...
@bp.route("/csp_violations", methods=["GET", "POST"])
def report():
content = request.get_json(force=True)
...
没有force=True
@bp.route("/csp_violations", methods=["GET", "POST"])
def report():
content = request.get_json() # json.decoder.JSONDecodeError
...
Chromium(在 Chrome、Brave 和 FireFox 上结果相同)
当我在 CTRL-SHIFT-I > 网络下的 Chromium 上查看时,我看到
Request URL: https://127.0.0.1:5000/report/csp_violations
Request Method: POST
Status Code: 400 BAD REQUEST
Remote Address: 127.0.0.1:5000
Referrer Policy: strict-origin-when-cross-origin
但显然负载确实存在于“请求负载”下的底部...
{
"document-uri": "https://127.0.0.1:5000/",
"referrer": "",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "default-src 'self' https://cdnjs.cloudflare.com https://cdn.cloudflare.com https://cdn.jsdelivr.net https://gravatar.com jquery.js; report-uri /report/csp-violations",
"disposition": "enforce",
"blocked-uri": "inline",
"line-number": 319,
"source-file": "https://127.0.0.1:5000/",
"status-code": 200,
"script-sample": ""
}
CSP 是否阻止了此请求?
看完
这修复了一些单元测试,但没有改变这条特定路线的 400 错误
# app/config.py
from environs import Env
from flask import Flask
env = Env()
env.read_env()
class Config:
"""Load environment."""
...
@property
def PREFERRED_URL_SCHEME(self) -> str: # noqa
return env.str("PREFERRED_URL_SCHEME", default="https")
...
# app/__init__.py
from app.config import Config
def create_app() -> Flask:
app = Flask(__name__)
app.config.from_object(Config())
...
return app
flask run --cert="$HOME/.openssl/cert.pem" --key="$HOME/.openssl/key.pem"
400 错误请求
据我所知,400 Bad Request 错误通常发生在空请求或表单中
...the issue is that Flask raises an HTTP error when it fails to find a key in
the args and form dictionaries. What Flask assumes by default is that if you
are asking for a particular key and it's not there then something got
left out of the request and the entire request is invalid.
99% of the time, this error is a key error caused by your requesting a key in
the request.form dictionary that does not exist. To debug it, run
print(request.form)
request.data
就我而言
通过尝试解决这个问题,我已经了解了 rabbit-hole 只是看到 400 错误的原因
我已经实现了以下
import traceback
from flask import Flask
...
app = Flask(__name__)
...
@app.errorhandler(400)
def internal_error(exception): # noqa
print("400 error caught")
print(traceback.format_exc())
并在我的配置中添加了以下内容
我没有检查过,但我认为这些值已经与 DEBUG
# app/config.py
from environs import Env
env = Env()
env.read_env()
class Config:
"""Load environment."""
...
@property
def TRAP_HTTP_EXCEPTIONS(self) -> bool: # noqa
"""Report traceback on error."""
return env.bool("TRAP_HTTP_EXCEPTIONS", default=self.DEBUG) # noqa
@property
def TRAP_BAD_REQUEST_ERRORS(self) -> bool: # noqa
"""Report 400 traceback on error."""
return env.bool("TRAP_BAD_REQUEST_ERRORS", default=self.DEBUG) # noqa
...
我仍然没有得到追溯。 werkzeug 唯一一次显示回溯是通过完全崩溃,例如语法错误
看来,因为我没有初始化这个请求,应用程序继续运行通过400代码,没问题
总结
我得出的主要结论是,出于某种原因,report-uri
无效,因为我可以看到有效负载存在,我只是没有收到它
我使用了相对路由,因为本地主机和远程主机的子域会有所不同。不过,看起来好像是在 Chromium 代码段中向完整的 URL 发出请求。
我会收到无效的 headers
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri
注意: report-uri
已弃用,但我认为大多数浏览器不支持 report-to
参数
编辑:
我已经从我的应用程序工厂中取出以下内容 (此代码呈现我的异常 - 我 post 在此处将其作为答案编辑,因此它可用
...
app = Flask(__name__)
config.init_app(app)
...
# exceptions.init_app(app)
...
return app
在这样做之后(在 FireFox 私有模式下),我做了另一个 POST(我已经删除了 GET 方法) 我可以看到响应 - 在这里我终于设法收集了一个 Werkzeug 回溯:
wtforms.validators.ValidationError: The CSRF token is missing.
研究如何验证来自浏览器的 csp-report
到目前为止我已经看过这个:
试试这段代码:
@app.route('/report-csp-violations', methods=['POST'])
def report():
content = request.get_json(force=True) # that's where the shoe pinches
print(json.dumps(content, indent=4, sort_keys=True))
response = make_response()
response.status_code = 204
return response
我认为 request.data
会尝试自动解析 JSON 数据,为了成功解析,它需要发送 application/json
MIME 类型。
但是违规报告是用 application/csp-report
MIME 类型发送的,因此 Flask 将其视为来自客户端的错误数据 -> 404 Bad Request。
.get_json(force=True)
表示忽略 mimetype 并始终尝试解析 JSON.
此外,我认为您不需要转换为 utf-8,因为根据 rfc4627“3. 编码”:
JSON 文本应使用 Unicode 编码。默认编码为 UTF-8。
目前我已将视图从 CRSFProtect
中排除。
我认为应该没问题,因为此视图没有 return 模板,所以我无法添加
<form method="post">
{{ form.csrf_token }}
</form>
下面的内容对我不起作用(不要认为它是有效的标记)
# app/routes/report.py
...
def report():
response = make_response()
response.headers["X-CSRFToken"] = csrf.generate_csrf()
...
return response
...
使用实例化的 CSRFProtect
对象...
# app/extensions.py
...
from flask_wtf.csrf import CSRFProtect
...
csrf_protect = CSRFProtect()
...
def init_app(app: Flask) -> None:
...
csrf_protect.init_app(app)
...
...
...装饰视图
# app/routes/report.py
from app.extensions import csrf_protect
...
@blueprint.route("/csp_violations", methods=["POST"])
@csrf_protect.exempt
def report():
....
127.0.0.1 - - [06/Nov/2021 21:30:46] "POST /report/csp_violations HTTP/1.1" 204 -
{
"csp-report": {
"blocked-uri": "inline",
"column-number": 8118,
"document-uri": "https://127.0.0.1:5000/",
"line-number": 3,
"original-policy": "default-src" ...
"referrer": "",
"source-file": ...
}
}
我有一个系统正在运行,我的错误被混淆了,所以这是我的错误。这不是我第一次遇到 CSRFProtect
的问题。
我已经用
解决了调试问题# app/exceptions.py
...
def init_app(app: Flask) -> None:
...
def render_error(error: HTTPException) -> Tuple[str, int]:
# the below code is new
app.logger.error(error.description)
...
...
所以,如果我仍然收到此错误,这就是我现在看到的内容
[2021-11-06 21:23:56,054] ERROR in exceptions: The CSRF token is missing.
127.0.0.1 - - [06/Nov/2021 21:23:56] "POST /report/csp_violations HTTP/1.1" 400 -