跳转至内容

CSRF 保护

简介

跨站请求伪造(CSRF)是一种恶意攻击,它允许攻击者以已认证用户的名义执行未经授权的命令。值得庆幸的是,Laravel 可以轻松保护您的应用程序免受 跨站请求伪造 (CSRF) 的攻击。

漏洞解释

如果您不熟悉跨站请求伪造,我们来探讨一个关于该漏洞如何被利用的示例。假设您的应用程序有一个 /user/email 路由,该路由接受 POST 请求以更改已认证用户的电子邮件地址。最有可能的情况是,此路由期望 email 输入字段包含用户希望开始使用的电子邮件地址。

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

1<form action="https://your-application.com/user/email" method="POST">
2 <input type="email" value="[email protected]">
3</form>
4 
5<script>
6 document.forms[0].submit();
7</script>

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

为了防止此漏洞,我们需要检查每个传入的 POSTPUTPATCHDELETE 请求,确认其是否包含恶意应用程序无法获取的加密会话值。

防止 CSRF 请求

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

首先,该中间件会检查浏览器的 Sec-Fetch-Site 请求头。现代浏览器会在每次请求时自动设置此标头,以表明请求是源自相同源、相同站点还是跨站来源。如果标头显示请求来自相同源,则无需任何令牌验证即可直接允许该请求。

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

Laravel 会自动为应用程序管理的每个活跃的 用户会话 生成一个 CSRF “令牌”。该令牌用于验证发出请求的用户是否正是已认证的本人。由于此令牌存储在用户的会话中,并且在每次重置会话时都会更改,因此恶意应用程序无法获取它。

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

1use Illuminate\Http\Request;
2 
3Route::get('/token', function (Request $request) {
4 $token = $request->session()->token();
5 
6 $token = csrf_token();
7 
8 // ...
9});

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

1<form method="POST" action="/profile">
2 @csrf
3 
4 <!-- Equivalent to... -->
5 <input type="hidden" name="_token" value="{{ csrf_token() }}" />
6</form>

CSRF 令牌与 SPA

如果您正在构建一个使用 Laravel 作为 API 后端的 SPA(单页应用),请参阅 Laravel Sanctum 文档,了解有关 API 身份验证及防止 CSRF 漏洞的信息。

源验证

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

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

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

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

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

如果您的应用程序需要接受来自子域的请求(例如,dashboard.example.com 接受来自 example.com 的请求),除了允许相同源请求外,您还可以允许相同站点请求。

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

从 CSRF 保护中排除 URI

有时您可能希望将一组 URI 从 CSRF 保护中排除。例如,如果您正在使用 Stripe 处理支付并利用其 webhook 系统,则需要将您的 Stripe webhook 处理路由从 CSRF 保护中排除,因为 Stripe 不知道要向您的路由发送什么 CSRF 令牌。

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

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

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

X-CSRF-TOKEN

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

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

然后,您可以指示 jQuery 之类的库自动将该令牌添加到所有请求标头中。这为使用旧版 JavaScript 技术的 AJAX 应用程序提供了简单、便捷的 CSRF 保护。

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

X-XSRF-TOKEN

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

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

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