CSRF 保护
CSRF 保护(CSRF Protection)
简介
跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种恶意攻击方式,攻击者会在用户已认证(已登录)的情况下,冒充用户执行未授权的操作。
幸运的是,Laravel 可以非常轻松地保护你的应用免受 Cross-Site Request Forgery (CSRF)攻击。
漏洞原理说明
如果你对跨站请求伪造(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 请求,以验证其中是否包含恶意应用程序无法访问的秘密 Session 值。
防止 CSRF 请求
默认包含在 web 中间件组中的 Illuminate\Foundation\Http\Middleware\PreventRequestForgery 中间件,通过双层方式保护你的应用程序免受跨站请求伪造(CSRF)攻击。
首先,该中间件会检查浏览器的 Sec-Fetch-Site 请求头。现代浏览器会在每个请求中自动设置此请求头,用于指示请求是否来自相同源(same origin)、相同站点(same site)或跨站来源(cross-site source)。如果请求头表明请求来自相同源,则会立即允许该请求,而无需进行任何 token 验证。
如果来源验证未通过 —— 例如,请求来自不发送 Sec-Fetch-Site 请求头的旧版浏览器,或者连接不安全 —— 中间件将回退到传统的 CSRF token 验证方式。
Laravel 会为应用程序管理的每个活动 用户会话 自动生成一个 CSRF “token”。此 token 用于验证已认证用户是否确实是向应用程序发起请求的人。由于该 token 存储在用户 session 中,并且每次 session 重新生成时都会发生变化,因此恶意应用程序无法访问它。
当前 session 的 CSRF token 可以通过请求的 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 指令来生成隐藏的 token 输入字段:
<form method="POST" action="/profile">
@csrf
<!-- 等价于... -->
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>
CSRF Token 与 SPA
如果你正在构建一个使用 Laravel 作为 API 后端的 SPA(单页应用程序),你应该查阅 Laravel Sanctum 文档,以获取有关 API 身份验证以及防止 CSRF 漏洞的信息。
来源验证(Origin Verification)
如上所述,Laravel 的请求伪造中间件首先会检查 Sec-Fetch-Site 请求头,以确定请求是否来自相同源。
默认情况下,如果该检查未通过,中间件将回退到 CSRF token 验证。
不过,如果你希望完全依赖来源验证,并完全禁用 CSRF token 的回退验证机制,你可以在应用程序的 bootstrap/app.php 文件中使用 preventRequestForgery 方法:
->withMiddleware(function (Middleware $middleware): void {
$middleware->preventRequestForgery(originOnly: true);
})
当使用仅来源验证模式(origin-only mode)时,来源验证失败的请求将收到 403 HTTP 响应,而不是通常与 CSRF token 不匹配相关联的 419 响应。
[!警告]
Sec-Fetch-Site请求头仅会由浏览器在安全(HTTPS)连接中发送。如果你的应用程序未通过 HTTPS 提供服务,则来源验证将不可用,中间件将回退到 CSRF token 验证。
如果你的应用程序需要接受来自子域名的请求(例如,dashboard.example.com 接受来自 example.com 的请求),你可以允许同站点(same-site)请求,而不仅仅是相同源(same-origin)请求:
->withMiddleware(function (Middleware $middleware): void {
$middleware->preventRequestForgery(allowSameSite: true);
})
从 CSRF 保护中排除 URI
有时候,你可能希望将一组 URI 排除在 CSRF 保护之外。例如,如果你正在使用 Stripe 处理支付,并使用它们的 webhook 系统,那么你需要将 Stripe webhook 处理路由从 CSRF 保护中排除,因为 Stripe 不会知道应该向你的路由发送什么 CSRF token。
通常,你应该将这类路由放在 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
除了将 CSRF token 作为 POST 参数进行检查之外,PreventRequestForgery 中间件还会检查 X-CSRF-TOKEN 请求头。例如,你可以将 token 存储在 HTML 的 meta 标签中:
<meta name="csrf-token" content="{{ csrf_token() }}">
然后,你可以指示像 jQuery 这样的库自动将 token 添加到所有请求头中。这为基于 AJAX 的应用程序使用传统 JavaScript 技术提供了简单且方便的 CSRF 防护:
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
X-XSRF-TOKEN
Laravel 会将当前的 CSRF token 存储在一个加密的 XSRF-TOKEN Cookie 中,并随框架生成的每个响应一起返回。你可以使用该 Cookie 的值来设置 X-XSRF-TOKEN 请求头。
该 Cookie 主要是为了方便开发者,因为某些 JavaScript 框架和库(例如 Angular 和 Axios)会在同源请求中自动将其值放入 X-XSRF-TOKEN 请求头中。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Laravel 13 中文文档
关于 LearnKu
推荐文章: