从不涉及 Flask 表单的 PUT 获取请求中获取 CSRF 错误

问题描述

我在我的 Flask web 应用程序中收到一个 CSRF 错误,来自一个常规的 PUT 请求。
我不明白为什么会出现这些错误,因为 PUT 请求不涉及任何形式。

我遵循了文档 here 并使用 CSRF 保护初始化我的应用程序,如下所示:

cat app/__init__.py
...
from flask_wtf.csrf import CSRFProtect,generate_csrf,validate_csrf
...
csrf = CSRFProtect()
...
def create_app(config_class=Config):
    app = Flask(__name__)
...
    csrf.init_app(app)
...

来自javascript的调用

cat js/js_script1.js
...
        queryUrl = 'https://localhost/put_request1';
        let fetchData = { 
            method: 'PUT'
        };
        let response = await fetch(queryUrl,fetchData);

我在浏览器和后端看到错误 - Error: 400 Bad Request: The CSRF token is missing.

文档主要讨论表单上下文中的 CSRF 和 ajax,但在这种情况下,我使用的是 fetch(对于这件事,fetch 是否与 ajax 相同?)并且没有任何表单。

this link 部分:CSRF 令牌是如何构建的 有一段代码片段,它使用 flask_wtf 函数 generate_csrf 来创建 csrf_token。 我试图在 app/__init__.py 中实现它,这导致了其他不相关的错误 (RuntimeError: Working outside of request context)。

here 中,csrf_token 是从 html 页面中的“csrf-token”元素读取的。

const csrftoken = document.querySelector("[name='csrf-token']").content

但我没有 csrf 令牌

为了进行比较,我打开了另外一个有表单的页面,在那里我确实看到了预期的 csrf_token 元素:

<input id="csrf_token" name="csrf_token" type="hidden" value="ImI...">

chrome 调试器 -> 应用程序 -> cookie -> https://localhost cookie 中,我只看到 session 令牌但看不到 csrf_token

问题:

  • 在初始化 Flask 应用程序时是否需要生成 csrf_token?
    (据我了解,Flask 会自动为 FlaskForm 执行此操作,但在这种情况下,我没有涉及 FlaskForm)
  • 如果我确实需要生成 csrf_token,我该如何为常规 POST/PUT 提取请求生成 csrf_token?

编辑 1:

根据@Detlef 的回复,我又做了一次尝试。
我尝试按照 here 中的 Javascript 请求指南使用以下命令获取 csrf_token 失败: var csrf_token = "{{ csrf_token() }}";

我知道如果此代码嵌入在 html 页面中,那么 jinja2 将从函数 csrf_token() 返回的值替换为变量 csrf_token

就我而言,我从 javascript 文件(包含在 html 页面中)发出提取请求。
所以我不明白在这种情况下如何处理命令。

我对 javascript 代码进行了以下更改:

        var csrf_token = "{{ csrf_token() }}";
        console.log('csrf_token',csrf_token); 
        
        let headersData = {
            'Content-Type': 'application/json','X-CSrftoken': csrf_token,'X-CSRF-TOKEN': csrf_token,'X-CSRF-Token': csrf_token,};

        let fetchData = { 
            method: 'PUT',headers: headersData
        };

        queryUrl = 'https://localhost/put_request1';
        let response = await fetch(queryUrl,fetchData);

显示在控制台中的 csrf_token 变量的值是
csrf_token: {{ csrf_token() }}
这显然是错误的(没有任何解释)。
这会导致浏览器和后端出现另一个 CSRF 错误

Error: 400 Bad Request: The CSRF token is invalid.

编辑 2:

根据@Detlef I 的指导:

<Meta content="{{ csrf_token() }}" name="csrf-token">
  • 使用以下方法在 javascript 中检索 csrf_token:
const csrftoken = document.querySelector("Meta[name='csrf-token']").getAttribute("content");

解决了问题。
我没有看到 CSRF 错误
我还在 chrome-devtools -> Networks 中检查了失败的请求,我确实在请求标头 x-csrf-token 中看到了预期的内容

x-csrf-token: ImI0ODY...

解决方法

javascript fetch api 应该使 ajax 请求更容易。
所以,是的 fetch 是一个 ajax 请求。

在您的 Flask 应用程序初始化期间不会生成 CSRF 令牌。它更依赖于请求。关键是,攻击者可以接管其他人的会话并代表他们发送请求。据我所知,它应该是每个请求令牌,并确保对方客户端仍然是发出前一个 GET 请求的那个人。请参见this
关于何时以及如何更新与 ajax 请求相关的 CSRF 令牌有各种讨论。我不是安全专家,目前不想参与其中。

将令牌保存在 html 元元素中是 我认为这是一种明智的方法,就我忽略的情况而言,它可以用于您的情况。您也可以将令牌直接分配给 javascript 变量。
只需使用 "JavaScript Requests" in the documentation 下提到的 csrf_token() 函数。然后应将令牌添加到“X-CSRFToken”下的请求标头中。


不能在 jinja2 模板之外使用命令 csrf_token()。 如果可能,将令牌作为参数传递或使用描述的 html 元标记。

将此添加到您的模板中。

<meta content="{{ csrf_token() }}" name="csrf-token">

现在在您的脚本文件中使用它。

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");