Skip to content

CSRF 保护

简介

跨站请求伪造是一种恶意利用方式,其中未经授权的命令代表经过身份验证的用户执行。幸运的是,Laravel 使保护您的应用程序免受 跨站请求伪造(CSRF)攻击变得容易。

漏洞说明

如果您不熟悉跨站请求伪造,让我们讨论一个如何利用此漏洞的示例。假设您的应用程序有一个 /user/email 路由,该路由接受 POST 请求来更改经过身份验证用户的电子邮件地址。很可能,此路由期望一个 email 输入字段包含用户希望开始使用的电子邮件地址。

如果没有 CSRF 保护,恶意网站可以创建一个指向您应用程序 /user/email 路由的 HTML 表单,并提交恶意用户自己的电子邮件地址:

blade
<form action="https://your-application.com/user/email" method="POST">
    <input type="email" value="malicious-email@example.com">
</form>

<script>
    document.forms[0].submit();
</script>

如果恶意网站在页面加载时自动提交表单,恶意用户只需引诱您应用程序的无辜用户访问他们的网站,他们的电子邮件地址就会在您的应用程序中被更改。

为了防止此漏洞,我们需要检查每个传入的 POSTPUTPATCHDELETE 请求中是否包含恶意应用程序无法访问的秘密会话值。

防止 CSRF 请求

Illuminate\Foundation\Http\Middleware\PreventRequestForgery 中间件,默认包含在 web 中间件组中,使用双层方法保护您的应用程序免受跨站请求伪造。

首先,中间件检查浏览器的 Sec-Fetch-Site 头。现代浏览器在每个请求上自动设置此头,指示它是来自同源、同站还是跨站源。如果头指示请求来自同源,则立即允许请求,无需任何令牌验证。

如果来源验证未通过——例如,因为请求来自不发送 Sec-Fetch-Site 头的旧浏览器,或者连接不安全——中间件将回退到传统的 CSRF 令牌验证。

Laravel 为应用程序管理的每个活动 用户会话 自动生成一个 CSRF"令牌"。此令牌用于验证经过身份验证的用户是实际向应用程序发出请求的人。由于此令牌存储在用户会话中,并且每次会话重新生成时都会更改,恶意应用程序无法访问它。

可以通过请求的会话或 csrf_token 辅助函数访问当前会话的 CSRF 令牌:

php
use Illuminate\Http\Request;

Route::get('/token', function (Request $request) {
    $token = $request->session()->token();

    $token = csrf_token();

    // ...
});

每当您在应用程序中定义"POST"、"PUT"、"PATCH"或"DELETE" HTML 表单时,您应该在表单中包含一个隐藏的 CSRF _token 字段,以便 CSRF 保护中间件可以验证请求。为方便起见,您可以使用 @csrf Blade 指令生成隐藏的令牌输入字段:

blade
<form method="POST" action="/profile">
    @csrf

    <!-- 等同于... -->
    <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>

CSRF 令牌与 SPA

如果您正在构建一个使用 Laravel 作为 API 后端的 SPA,您应该查阅 Laravel Sanctum 文档 以获取有关使用 API 进行身份验证和防止 CSRF 漏洞的信息。

来源验证

如上所述,Laravel 的请求伪造中间件首先检查 Sec-Fetch-Site 头以确定请求是否来自同源。默认情况下,如果此检查未通过,中间件将回退到 CSRF 令牌验证。

但是,如果您希望仅依赖来源验证并完全禁用 CSRF 令牌回退,可以在应用程序的 bootstrap/app.php 文件中使用 preventRequestForgery 方法:

php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(originOnly: true);
})

使用仅来源模式时,未通过来源验证的请求将收到 403 HTTP 响应,而不是通常与 CSRF 令牌不匹配关联的 419 响应。

WARNING

Sec-Fetch-Site 头仅由浏览器通过安全(HTTPS)连接发送。如果您的应用程序不是通过 HTTPS 提供服务,来源验证将不可用,中间件将回退到 CSRF 令牌验证。

如果您的应用程序需要接受来自子域的请求(例如,dashboard.example.com 接受来自 example.com 的请求),您可以允许同站请求以及同源请求:

php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(allowSameSite: true);
})

从 CSRF 保护中排除 URI

有时您可能希望从 CSRF 保护中排除一组 URI。例如,如果您使用 Stripe 处理付款并使用他们的 webhook 系统,您需要从 CSRF 保护中排除您的 Stripe webhook 处理程序路由,因为 Stripe 不会知道要向您的路由发送什么 CSRF 令牌。

通常,您应该将这些类型的路由放在 Laravel 应用于 routes/web.php 文件中所有路由的 web 中间件组之外。但是,您也可以通过在应用程序的 bootstrap/app.php 文件中将其 URI 提供给 preventRequestForgery 方法来排除特定路由:

php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
        'http://example.com/foo/bar',
        'http://example.com/foo/*',
    ]);
})

NOTE

为方便起见,运行测试 时,CSRF 中间件对所有路由自动禁用。

X-CSRF-TOKEN

除了检查作为 POST 参数的 CSRF 令牌外,PreventRequestForgery 中间件还会检查 X-CSRF-TOKEN 请求头。例如,您可以将令牌存储在 HTML meta 标签中:

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

然后,您可以指示像 jQuery 这样的库自动将令牌添加到所有请求头中。这为使用传统 JavaScript 技术的基于 AJAX 的应用程序提供了简单、方便的 CSRF 保护:

js
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

X-XSRF-TOKEN

Laravel 将当前 CSRF 令牌存储在加密的 XSRF-TOKEN cookie 中,该 cookie 包含在框架生成的每个响应中。您可以使用 cookie 值设置 X-XSRF-TOKEN 请求头。

此 cookie 主要作为开发者的便利发送,因为一些 JavaScript 框架和库(如 Angular 和 Axios)会自动将其值放在同源请求的 X-XSRF-TOKEN 头中。

NOTE

默认情况下,resources/js/bootstrap.js 文件包含 Axios HTTP 库,它会自动为您发送 X-XSRF-TOKEN 头。