简介

跨站请求伪造是一种恶意攻击,即代表已认证用户执行未授权命令。值得庆幸的是,Laravel 可以轻松保护你的应用免受跨站请求伪造(CSRF)攻击。

漏洞解析

如果你对跨站请求伪造不太熟悉,我们来讨论一个可利用此漏洞的示例。假设你的应用有一个/user/email路由,该路由接受POST请求以更改已认证用户的电子邮件地址。该路由很可能期望email输入字段包含用户想要开始使用的电子邮件地址。

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

<form action="https://your-application.com/user/email" method="POST">
    <input type="email" value="[email protected]">
</form>

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

如果恶意网站在页面加载时自动提交表单,恶意用户只需诱使应用中毫无防备的用户访问其网站,该用户的电子邮件地址就会被更改。

为了防范此漏洞,我们需要检查每个传入的POST、PUT、PATCH、DELETE或QUERY请求,确认其中是否存在恶意应用无法访问的机密会话值。

防范 CSRF 请求

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

首先,中间件会检查浏览器的Sec-Fetch-Site标头。现代浏览器会在每个请求中自动设置该标头,用于表明请求是否来自同一源、同一站点或跨站来源。如果标头显示请求来自同一源,则无需任何令牌验证,请求会被立即放行。

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

Laravel 会为应用管理的每个活跃用户会话自动生成一个 CSRF “令牌”。此令牌用于验证已认证用户是否为实际向应用发起请求的人。由于该令牌存储在用户会话中,且每次会话重新生成时都会发生变化,因此恶意应用无法访问它。

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

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 指令生成隐藏的令牌输入字段:

<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方法来实现:

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

在仅使用源验证模式下,未通过源验证的请求会收到403 HTTP 响应,而非通常与 CSRF 令牌不匹配相关的419响应。

浏览器仅在安全连接(HTTPS)下才会发送Sec-Fetch-Site标头。如果你的应用未通过 HTTPS 提供服务,将无法使用源验证,中间件会回退到 CSRF 令牌验证。

如果你的应用需要接受来自子域的请求(例如,dashboard.example.com接受来自example.com的请求),可以在允许同源请求的基础上,额外允许同站点请求:

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

将 URI 排除在 CSRF 保护之外

有时你可能希望将部分 URI 排除在 CSRF 保护之外。例如,如果你使用Stripe处理支付,且使用了其 Webhook 系统,就需要将 Stripe Webhook 处理路由排除在 CSRF 保护之外,因为 Stripe 不知道该向路由发送什么 CSRF 令牌。

通常,应将此类路由放在 Laravel 应用于routes/web.php文件中所有路由的web中间件组之外。不过,也可以在应用的bootstrap/app.php文件中,将特定路由的 URI 传入preventRequestForgery方法,从而排除这些路由:

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

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

X-CSRF-TOKEN

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

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

然后,可指示 jQuery 等库自动将令牌添加到所有请求标头中。这样,在使用传统 JavaScript 技术的 AJAX 应用中,就能实现简单便捷的 CSRF 保护:

$.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 主要是为开发者提供便利,因为 Angular、Axios 等部分 JavaScript 框架和库,会自动将其值放入同源请求的X-XSRF-TOKEN标头中。