CSRF 保护
跨站请求伪造(Cross-Site Request Forgery)是一种攻击者恶意利用在已认证用户的身份下执行未授权的操作。幸运的是,Laravel 让你可以轻松抵御跨站请求伪造(CSRF)攻击。
漏洞原理
如果你对跨站请求伪造不太熟悉,下面通过一个例子来讲解这个漏洞是如何被利用的。假设你的应用有一个 /user/email 路由,接受 POST 请求来修改已认证用户的邮箱地址。通常这个路由会期望一个 email 输入字段来包含用户想要使用的新邮箱地址。
如果没有 CSRF 保护,恶意网站可以创建一个 HTML 表单,指向你应用的 /user/email 路由,并提交攻击者自己的邮箱地址:
<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>
如果恶意网站在页面加载时自动提交该表单,攻击者只需要诱导你应用的某个已登录用户访问他们的网站,这个用户的邮箱地址就会在你的应用中被悄然篡改。
为了防御这种漏洞,我们需要对每一个 POST、PUT、PATCH 或 DELETE 请求进行检查,验证其中包含一个恶意应用无法获取的秘密会话值。
阻止 CSRF 请求
Illuminate\Foundation\Http\Middleware\PreventRequestForgery 中间件 默认包含在 web 中间件组中,通过双层机制保护你的应用免受跨站请求伪造攻击。
第一层:中间件检查浏览器的 Sec-Fetch-Site 请求头。现代浏览器会在每个请求上自动设置这个头,标明请求是来自同源(same-origin)、同站(same-site)还是跨站(cross-site)。如果该头标明请求来自同源,则直接放行,无需令牌验证。
如果来源验证未通过——例如请求来自不支持 Sec-Fetch-Site 头的旧版浏览器,或者连接不安全——中间件会回退到传统的 CSRF 令牌验证。
Laravel 会自动为应用中每个活跃的用户会话生成一个 CSRF "令牌"。这个令牌用于验证已认证用户确实是发起请求的那个人。由于令牌存储在用户会话中,并且在每次会话重新生成时都会变化,恶意应用无法获取它。
当前会话的 CSRF 令牌可以通过请求的 session 或 csrf_token 辅助函数获取:
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 响应。
注意
Sec-Fetch-Site 请求头仅在浏览器通过安全连接(HTTPS)访问时才会发送。如果你的应用未通过 HTTPS 提供服务,来源验证将不可用,中间件会回退到 CSRF 令牌验证。
如果你的应用需要接收来自子域名的请求(例如 dashboard.example.com 接收来自 example.com 的请求),可以在同源之外也允许同站请求:
->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 来排除特定路由:
->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 请求头中。