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 请求

默认包含在 web 中间件组中的 Illuminate\Foundation\Http\Middleware\PreventRequestForgery 中间件,会用双层策略保护应用免受跨站请求伪造。

首先,中间件会检查浏览器的 Sec-Fetch-Site 头。现代浏览器会在每个请求上自动设置该头,标明请求来自同源、同站还是跨站来源。若该头表明请求来自同源,则会立即放行,无需进行令牌验证。

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

Laravel 会为应用管理的每个活跃 用户会话 自动生成 CSRF「令牌」。该令牌用于验证向应用发起请求的确实是已认证用户本人。由于令牌保存在用户会话中,并会在会话重新生成时变化,恶意应用无法获取它。

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

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

    <!-- Equivalent to... -->
    <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

有时你可能希望将一组 URI 排除在 CSRF 保护之外。例如,若你使用 Stripe 处理支付并使用其 webhook 系统,就需要将 Stripe webhook 处理路由排除在 CSRF 保护之外,因为 Stripe 不知道应向你的路由发送什么 CSRF 令牌。

通常,这类路由应放在 Laravel 应用于 routes/web.php 中全部路由的 web 中间件组之外。不过,你也可以在应用的 bootstrap/app.php 文件中,通过向 preventRequestForgery 方法提供 URI 来排除特定路由:

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

INFO

为方便起见,运行测试 时,所有路由都会自动禁用 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 的值来设置 X-XSRF-TOKEN 请求头。

发送该 Cookie 主要是为了方便开发者,因为 Angular、Axios 等部分 JavaScript 框架和库会在同源请求时自动将其值放入 X-XSRF-TOKEN 头。