【安全防护】CSRF攻击到底有多狠?
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
大家好,事情得从一个面试说起,那天面试小伙子简历写着精通 Laravel 安全防护,我就问他:“你做过电商项目没?遇到过 CSRF 攻击不?”他挠挠头说理论懂,实战没碰过。我心想正好拿我前阵子的教训给他上一课,其实这教训是我刚毕业那会在开发时,因为一个不起眼的功能差点捅的篓子。 事情是这样的,那时候还在实习运营刚把新开发的【用户地址批量导入】功能推给内测用户。这功能基于 Laravel 9 开发,前端用 Vue 写了个简单的导入页面,后端用 Laravel 的 Resource Controller 处理 CSV 文件解析。测试时一切顺利,CSV 里的地址能正常入库,响应速度也快。结果内测群里突然炸了:“我明明没点导入,怎么购物车被清空了?”“刚才收到短信提示地址被修改,但我没操作啊!” 我赶紧查日志,好家伙,日志里一堆 POST /user/address/batch-import的请求,IP 地址五花八门,User-Agent 看着像爬虫,但关键是这些请求的 Session ID 还都是内测用户的。再仔细一看,这些请求的 Referer 头全是陌生的外部域名(后来查是测试那边伪造的钓鱼页面)。最惨的是,有个用户的购物车数据被恶意请求清空了(因为批量导入接口顺手做了“清空旧地址”的逻辑)。我当时脸就红了,事后复盘,我们犯了两个致命错误:一是低估了伪造请求的风险,以为内测环境没人攻击;二是没搞清楚 Laravel 自带的 CSRF 保护在 AJAX 和跨域场景下会失灵。这时候大家自然会想到加验证码?限制 IP?但这些都治标不治本。CSRF 攻击的核心,是攻击者冒充用户身份发起恶意请求,而 Laravel 其实早就准备了应对的机制,只是咱没用好。 一、先说 CSRF 是啥? 在聊 Laravel 的防护之前,咱得先把 CSRF 这玩意弄清楚。用大白话讲,CSRF就是冒充熟人骗开门,假设你在银行网站登录后,浏览器存了你的登录态,这时候你点开一个恶意链接,这个链接会偷偷向银行的转账接口发请求(比如转钱给攻击者)。因为浏览器会自动带上你的 Cookie,银行服务器一看Cookie 是对的,就默认本人操作,但实际上这请求是你不知情的被伪造的。 Laravel 框架本身是自带 CSRF 防护的(从 Laravel 5 开始就内置了 VerifyCsrfToken中间件),因为 Laravel 的防护机制默认只对同源请求生效,一旦涉及 AJAX、跨域、SPA 单页应用,或者你手贱改了默认配置,这层防护就可能形同虚设。举个咱项目的例子:那个批量导入接口用的是 AJAX 请求,前端为了图省事,没在请求头里带 CSRF Token,Laravel 的VerifyCsrfToken中间件一看这请求没带 Token,直接放行了,这就等于给攻击者留了个后门。 二、Token 是怎么校验的? 先带大家捋捋 Laravel 的 CSRF 防护流程。 1. Token 生成 当用户登录 Laravel 应用时,框架会在 Session 里生成一个唯一的 CSRF Token(默认 40 位随机字符串),并存到 session()->token()里。这个 Token 相当于用户的身份证,每次请求都要出示。 2. Token 传递 Laravel 要求所有【非GET】请求必须携带这个 Token,传递方式有三种:表单隐藏域、Meta 标签、请求头。 3. Token 验证 Laravel 的 App\Http\Middleware\VerifyCsrfToken中间件是所有请求的必经之路。它干两件事:检查请求方法:如果是 GET/HEAD/OPTIONS,直接放行;非安全方法:从请求中提取 Token,和 Session 里的 Token 比对,一致才放行。 这套机制听起来挺完美,但总会翻车,因为咱的 AJAX 请求既没带表单隐藏域,也没在 Header 里传 Token,中间件直接睁一只眼闭一只眼放了行,这就是典型的没按规矩出牌。 三、实战踩坑:Laravel CSRF 防护的 4 个大坑(附老王事故复盘) 接下来结合自己的踩坑经历,给大家盘点 Laravel 项目中最容易翻车的 CSRF 场景。 坑 1:AJAX 请求忘了带 Token 就是开头的地址导入接口。前端用 Vue 的 axios 发 POST 请求,后端 VerifyCsrfToken没拦住,导致攻击者伪造请求清空用户数据。Laravel 不会“自动”给 AJAX 请求加 Token。除非你手动配置。后端虽然启用了 VerifyCsrfToken中间件,但 axios 没传 Token,中间件比对时发现请求里没 Token,按理说应该拒绝,但咱当时为了调试方便,手贱改了中间件的 $except 数组,把这个接口排除了…… 解决方案:给 AJAX 请求加上 Token。分两种情况:
坑 2:跨域请求(CORS)导致 Token 验证失败 后来咱做了个 H5 活动页,前端部署在 CDN 域名(m.xxx.com),后端 API 在 api.xxx.com。用户点击活动页的领取优惠券按钮,前端用 axios 发 POST 请求到 api.xxx.com/coupon/receive,结果返回 419 。跨域请求时,浏览器会先发一个 OPTIONS 预检请求,检查服务器是否允许跨域。而 Laravel 的 VerifyCsrfToken中间件默认会对 OPTIONS 请求也做 Token 验证,但 OPTIONS 请求本身不会带 Token,自然就被拦了。 解决方案:配置 CORS 中间件,让 OPTIONS 请求跳过 CSRF 验证。Laravel 可以用 fruitcake/laravel-cors包,安装后修改 config/cors.php:
同时在 VerifyCsrfToken中间件的 $except数组里加上 OPTIONS 请求(或直接排除跨域接口):
坑 3:Token 过期或 Session 失效 有用户反馈,登录后点击“提交订单”按钮,偶尔会返回 419 错误,刷新页面后又好了。查日志发现,这些请求的 Session ID 是新的(说明 Session 过期了),但前端还在用旧的 CSRF Token。Laravel 的 CSRF Token 默认和 Session 绑定,Session 过期后,Token 也会失效。如果用户长时间停留在页面(比如填表单填了半小时),这时候提交,Token 已经失效,自然验证失败。 解决方案:两种思路,一是延长 Session 有效期,修改 config/session.php的 'lifetime'值,比如设为 1440,但会增加安全风险;二是动态刷新 Token,前端在每次请求成功后,主动从响应头或 Meta 标签获取新 Token(Laravel 会在 Session 续期时自动更新 Token,可通过 csrf_token()函数获取)。 推荐第二种,用 JS 监听 Token 过期,自动刷新:
坑 4:SPA 单页应用首次 Token 获取 后来咱用 React 重构了后台管理系统,登录后进入首页,点击菜单加载数据时,频繁出现 419 错误。排查发现,React 组件在挂载时就发起了请求,但此时 Blade 模板里的 Meta 标签还没渲染(因为是异步加载),导致前端拿不到 Token。SPA 应用通常是先加载一个空白 HTML,再通过 JS 动态渲染内容,如果 CSRF Token 是写在 Blade 模板里的 Meta 标签,可能在 JS 执行时还没生成,前端自然拿不到 Token。 解决方案:用 Laravel Sanctum 的CSRF Cookie接口主动获取 Token。Sanctum 是 Laravel 的轻量级 API 认证包,它提供了一个 /sanctum/csrf-cookie接口,访问后会返回一个包含 CSRF Token 的 Cookie(名为 XSRF-TOKEN),前端可以从 Cookie 里读取 Token 并设置到 Header。 四、进阶防护:除了 Token,还能怎么加固 CSRF 防护? CSRF Token 是 Laravel 防护的核心,但不是全部。再分享几个进阶技巧。 1. SameSite Cookie 属性:从源头减少“被伪造”的可能。 现代浏览器支持 Cookie 的 SameSite属性,它可以限制 Cookie 在跨站请求中的发送:
Laravel 可以在 .env文件中设置:
2. 二次验证:敏感操作再加一道锁 对于“转账”“修改密码”“批量删除”等高危操作,即使有 CSRF Token,也可以再加一层验证(比如短信验证码)。Laravel 可以用 laravel/fortify包集成双因素认证,或者在业务逻辑里手动校验:
3. 监控异常请求:用日志揪出攻击者 在项目里加了 CSRF 验证失败的日志记录,方便追踪攻击:
五、CSRF 防护的核心就一句话 => [别让攻击者冒充用户] 折腾了这一大圈,从数据被改到压测翻车,再到一步步填坑,最大的感悟是:CSRF 防护的本质,是验证“请求确实来自用户的主动操作”。Laravel 已经给了我们一把好锁,但能不能防住贼,还得看咱会不会用。安全从来不是有了就行,细节到位才是王道。 阅读原文:原文链接 该文章在 2025/12/18 9:35:18 编辑过 |
关键字查询
相关文章
正在查询... |