资讯动态

CSRF漏洞原理与修复实践:从攻击复现到防御落地

发布时间:2026/10/19 15:04:37 来源:尧图企业网站定制
CSRF这个缩写做Web开发的人多少都见过但真正在代码里把防护落地过的说实话不算多。我最早认真研究它是几年前帮某公司做内部安全评估发现一个后台管理系统的接口居然只校验了登录态从外部页面构造一个隐藏表单就能把管理员绑定的手机号改掉。那会儿我才意识到很多人对CSRF的理解还停留在“知道名字”的阶段一旦要讲清楚攻击链路、要写修复代码就开始含糊。这篇文章不打算复述文档而是从一次本地实验讲起怎么复现、怎么理解、怎么修最后把我在实际项目里踩过的坑一起列出来。1. CSRF漏洞原理剖析1.1 一次典型的CSRF攻击流程CSRF的全称是Cross-Site Request Forgery跨站请求伪造。它的攻击链路看起来很简单但每一步都有历史原因。假设用户已经登录了mybank.test浏览器里保存了会话Cookie。用户之后打开另一个标签页访问攻击者准备好的页面evil.test。这个页面里可能只有一张隐藏表单或者一张图片目标是向mybank.test的转账接口发起请求。浏览器在发送请求时会根据目标域名自动带上对应的Cookie它不会关心这个请求是从哪个页面发起的。于是目标服务器看到的请求里带着合法会话和业务参数就当成正常用户操作执行了。整个过程里用户毫无察觉攻击者也读不到目标站点的响应。攻击者做的事情只是“指挥”浏览器发出一个请求。CSRF的本质是服务器过度信任了“浏览器自动带过来的身份凭证”默认这是用户主动发起的操作。很多人会有一个疑问浏览器不是有同源策略吗为什么还能向其他站点发请求这里要分清同源策略限制的是脚本读取跨域响应并不限制发送跨域请求。表单提交、图片加载、脚本标签、跳转、XHR和fetch发送请求这些行为本身就是被允许的。如果服务器不校验来源单靠浏览器机制是挡不住CSRF的。1.2 为什么CSRF能绕过同源策略用一个生活化的类比来说餐厅服务员看到你从钱包里掏出一张会员卡就默认你是本人。其实卡片可能只是被人贴在你后背带进来的。浏览器自动携带Cookie就像这张会员卡目标服务器只认卡不认人。从技术层面拆解CSRF能成立需要两个前提。第一个前提是用户必须在目标站点处于登录态而且登录凭证保存在Cookie里浏览器会自动携带。第二个前提是目标接口没有做来源合法性校验也没有使用随机Token这样的防伪标识。两个条件缺一个攻击都不能成立。可能有人会问攻击者为什么不能直接读取Cookie因为Cookie受同源策略保护跨域脚本读不到。攻击者也不需要读他只要让请求带着Cookie发出去就够了。这就是CSRF和一般“偷Cookie”型攻击的区别。此外以Cookie为主的登录态是最容易受到CSRF影响的。如果登录态改用自定义Header里的Token浏览器不会自动带上CSRF就很难成立。理解了这一点后面讲修复策略时会顺理成章。1.3 CSRF和XSS、CORS的边界CSRF经常和XSS混在一起但两者是不同维度的问题。XSS是攻击者往页面里注入了脚本相当于攻击者的“手”伸进了你的网页可以读取页面内容、操作DOM、盗取数据。CSRF是攻击者借你的浏览器发请求相当于“遥控”你的身份但看不到你页面里的东西。修复思路也不同。XSS要做的是过滤输入、转义输出、隔离不可信内容CSRF要做的是验证请求来源、加入一次性令牌、合理设置Cookie属性。如果只修其中一个另一个依然会形成安全漏洞甚至互相放大。CORS和CSRF也很容易搞混。CORS是服务器声明“允许哪些来源读取跨域响应”它影响的是攻击者能不能看到返回结果。如果CORS配置不当攻击者可以用fetch跨域发起请求并读取响应这让CSRF的危害从“执行操作”升级成“窃取数据”。但CORS本身不是CSRF只是可能让CSRF变得更危险。2. 案例演示从零构造可复现的CSRF场景2.1 准备两个本地站点纸上谈兵没意思我习惯在本地搭一对“靶子”来验证。一个站点作为目标一个站点作为攻击者两者用不同的域名这样才符合真实的跨站场景。目标站我用mybank.test攻击者站用evil.test。为什么不用localhost加不同端口因为现代浏览器判断“站”和“源”时端口不影响“站”的判定。localhost:8001和localhost:8002会被当作同一个站无法模拟跨站请求。所以我在本机hosts文件里加上两行把这两个域名都解析到127.0.0.1然后在两个端口分别启动服务。需要特别说明这个实验必须在自己的本地环境做不要对任何线上系统测试。所有域名都是我随便起的虚构地址纯粹用于教学演示。2.2 目标接口的粗糙实现为了让问题看得清楚目标接口只做一件事接收一个POST请求完成“转账”。我先给出一版没有任何防CSRF措施的实现代码用伪代码写你可以很方便地套到熟悉的Web框架里。# 转账接口初始版本只校验登录态没有任何防CSRF措施 # 伪代码示意换成你正在用的框架即可 route(/transfer, methods[POST]) def transfer(req): to req.form.get(to) amount req.form.get(amount) # 只检查了登录态没有检查请求来源 if req.session is None or req.session.get(uid) is None: return 401, 请先登录 # 执行业务逻辑 write_transfer_log(req.session[uid], to, amount) return 转账成功这段代码最典型的漏洞在于它只校验了“有没有登录”没有校验“是不是本站页面发起的操作”。现实中不少接口比这还糙登录态交给框架业务参数直接消费完全没有来源合法性判断。2.3 攻击页面的两种构造方式攻击者页面可以很简单。如果目标接口接受POST最常见的构造是自动提交表单。html body form actionhttp://mybank.test/transfer methodPOST idevilForm input typehidden nameto valueattacker input typehidden nameamount value10000 /form script document.getElementById(evilForm).submit(); /script /body /html页面加载时脚本自动提交表单。浏览器发起跨域POST请求自动带上mybank.test的Cookie。服务器看到合法会话和参数就执行了转账。如果目标接口接受GET构造更简单一张图片就够了img srchttp://mybank.test/transfer?toattackeramount10000浏览器加载图片时发起GET请求同样携带Cookie。所以我在评审代码时看到“写操作”用GET接口会直接判为高风险。这里有个技术点值得解释。对于POST接口为什么可以用form直接提交跨域请求因为form的默认编码是application/x-www-form-urlencoded浏览器允许这种跨域表单提交而且不需要CORS预检。虽然攻击脚本读不到响应但“发出请求”这一步完全合法服务器如果信任Cookie就上当了。2.4 复现实验以及如何观察请求把目标站和攻击者站都启动之后复现流程是这样的先用一个浏览器访问mybank.test登录并获得会话Cookie。保持登录状态在同一浏览器的新标签页访问evil.test的攻击页面。打开开发者工具的Network面板选中“保留日志”。刷新攻击页面观察那条发往/transfer的请求。正常情况下你会看到请求头里带着目标站的Cookie响应状态码是200业务执行成功。整个过程中浏览器、服务器都没有报错用户只看到页面一闪而过。如果实验没有复现成功最可能的原因是现代浏览器默认把Cookie按SameSiteLax处理跨站POST不会携带Cookie。为了还原历史场景需要临时把目标站种下的Cookie设置为SameSiteNone同时标记Secure。本地调试时可以用反向代理加一层自签名的HTTPS或者用支持Cookie属性配置的调试工具临时改一下。实验结束后要立刻恢复安全配置这种宽松设置放到生产环境就是自找麻烦。2.5 攻击者到底能拿到什么一个容易误导新手的点是攻击者全程看不到目标站点的响应也拿不到用户的会话Cookie。他发出的请求只是“被服务器当作合法操作”执行了。所以CSRF的危害不在“偷数据”而在“替用户做操作”。如果业务接口是改密码、改绑定手机、转账、发消息、删除数据攻击者就可以“借刀杀人”。要是目标接口存在CORS配置错误攻击者连响应都能读到那就从“执行操作”升级成“窃取数据”危害更大。3. 修复策略不只是加一个Token3.1 最核心的CSRF Token机制CSRF Token是目前最常用的防护手段核心思路是让请求带上一个“只有本站页面才能生成、攻击者无法提前获知”的随机值服务器在处理写操作前校验它。一次完整流程是用户访问页面时服务端生成一个足够随机的Token存放在会话中。渲染表单时把Token放进隐藏字段。用户提交表单时Token一起提交到服务端。服务端比对提交的Token与会话中的Token一致才执行业务。代码示意# 生成与校验Token的伪代码 import os import hmac # 用户会话开始或访问页面时生成 token os.urandom(32).hex() session[csrf_token] token # 表单提交后校验 submitted req.form.get(csrf_token) expected session.get(csrf_token, ) if not hmac.compare_digest(submitted, expected): return 403, CSRF校验失败有几个细节我特别想提醒。第一比较Token时用hmac.compare_digest或类似的常量时间比较函数不要用普通的等号比较避免因比较时间差泄露Token信息。第二同一会话的Token可以复用不必每次请求都旋转。如果每次生成页面都换一个新Token用户开多个标签页时后打开的页面会覆盖会话里的Token导致先打开的页面提交失败。这是个真实踩过坑的体验。第三不要把Token放在URL里。URL会出现在浏览器历史、日志和Referer里容易被泄露。隐藏字段或自定义Header是更合适的位置。第四不要把Token放在Cookie里就当完事。如果服务端只是比较“Cookie里的Token”和“请求里的Token”攻击者虽然无法预知Token但如果有子域Cookie注入、中间件改写或第三方脚本可写Cookie的风险这个方案就可能被绕过。严谨做法是把Token放在Session中由服务端统一管理。3.2 优先用框架或中间件而不是徒手造轮子主流的Web开发框架基本都有CSRF防护模块。修复CSRF的第一步不是自己写过滤器而是查一下框架有没有默认开启默认覆盖了哪些路由有没有漏掉非表单类型的接口。我见过很多“自己写了个Token过滤器结果漏掉一类接口”的案例。原因往往很简单团队自定义了Token生成逻辑但没有覆盖所有写操作路由或者写操作里混用了多个框架组件某些组件没走过滤器。使用框架中间件时还需要梳理接口清单。哪些接口是写操作哪些接口允许匿名访问哪些接口不需要CSRF保护都要登记清楚。比如纯开放API接口本来就没有Cookie认证可能不需要CSRF Token但需要有其他身份认证机制。3.3 SameSite Cookie现代浏览器自带的护城河SameSite是Cookie的一个属性告诉浏览器跨站请求时是否携带这个Cookie。三种取值差异很大Strict从其他站发起的请求一律不携带Cookie。防护最强但用户体验受影响比如用户从外部链接进入站内首次请求没有会话可能被引导重新登录。Lax允许顶层导航的GET请求携带Cookie其他跨站请求不携带。这是目前多数浏览器的默认值能拦住绝大多数基于POST的CSRF。None必须配合Secure而且任何跨站请求都会带上Cookie。相当于主动关闭了这层防护用了它就必须靠Token等其他手段兜底。2020年之后主流浏览器默认把没有指定SameSite的Cookie按Lax处理。这确实让CSRF的攻击面变小了但不能因此掉以轻心。有些老系统显式设置了SameSiteNone有些业务场景需要跨站携带Cookie仅靠默认值撑不住。修复时所有登录态Cookie都要显式设置SameSite推荐Lax或Strict。同时要注意Lax并不能单独作为完整防线因为GET型CSRF以及同站内部的攻击路径依然可能被利用Token校验不能省。3.4 校验Origin和Referer作为辅助除了Token和SameSite服务端还可以校验请求的Origin头。这个头由浏览器自动携带标识请求来源。对于跨站POST现代浏览器一般都会带上Origin。服务端维护一个精确的白名单只有允许的源才能执行写操作。白名单一定要做精确匹配不能只做包含匹配。比如允许的源是https://mybank.test如果把校验条件写成“包含mybank.test”攻击者用https://mybank.test.evil.com也能通过校验。Referer也可以参考但可靠性不如Origin。因为Referer可能被隐私策略省略、被重定向过程去掉有的浏览器配置也会屏蔽它。所以推荐把Referer作为辅助手段而不是唯一判断。还有一个简单有效的辅助手段要求写操作的请求头里带X-Requested-With: XMLHttpRequest这样的自定义头。跨站的普通表单提交无法设置这个头很多CSRF攻击会被直接拦下。不过这个手段不是万能的如果攻击者用XHR或者页面里的跨域配置支持自定义头依然可能绕过它只能增加攻击成本。3.5 修复后的回归验证清单修复不能停在“代码写了”要验证才可靠。我每次修复后都会过一遍这份清单正常从站内页面提交返回成功。直接访问攻击页面提交返回403。用curl构造不带Token的请求返回403。带错误Token返回403。带正确Token和合法会话返回成功。修改Origin头为非白名单返回403。同一会话开多个标签页继续操作提交不出现大面积失败。一个典型的验证命令curl -i -X POST http://mybank.test/transfer \ -H Origin: http://evil.test \ -d toattackeramount10000修复后的预期结果是403。再补一条带正确Token的请求预期结果是成功。4. 常见问题与排查技巧实录4.1 Token校验偶尔失败最常见的坑做了几年安全评估我总结出几个高频问题。第一个是页面缓存。浏览器缓存了旧表单页面用户看到的是旧Token会话里的Token已经变了提交时自然校验失败。对策是给表单页面设置合理的缓存策略不能长时间缓存动态页面。第二个是会话过期。用户停留时间过长Session里的Token还在但登录态已经失效前端没有及时拦截用户填完表单提交才发现失败。体验问题虽然不算漏洞但很容易让人误以为是Token实现有bug。第三个是多标签页冲突。程序每次生成页面都旋转Token导致最先打开的那个标签页里的Token失效。解决方法是在同一会话内复用Token不要频繁旋转。第四个是前后端分离时Token存放在一个会被第三方脚本读取的位置。如果页面存在XSSCSRF Token也会被读取等于绕过了保护。这提醒我们XSS和CSRF必须一起修不能孤立看待。排查这种问题时我一般分三步先看服务端日志里提交的Token和会话里的Token分别是什么再在浏览器里把表单隐藏字段的值、Application面板里的Session值打出来对比最后确认请求有没有被代理或安全网关改写。通常很快就能定位。4.2 前后端分离架构怎么处理CSRF传统表单靠隐藏字段提交TokenSPA和纯API项目就不能照搬了。实践中比较顺的方案是把Token放在自定义Header里。流程是用户登录后服务端返回一个CSRF Token。前端把Token存到内存或半持久化存储中。所有写请求在Header里带上X-CSRF-Token: token。服务端校验Header里的Token。这和表单字段的思路一致只不过换了个位置。要注意的是如果Token存到了localStorage而页面存在XSSlocalStorage里的Token就会被读取。所以更干净的方案是登录态不用Cookie改成纯Bearer Token认证由前端显式放到Header里浏览器不会自动携带CSRF自然消失。不过在项目改造成本较高时Cookie加Header Token的方案依然可用关键在于做好XSS防护。4.3 双提交Cookie到底能不能用双提交Cookie是一种不依赖服务端Session的方案服务端把Token种到Cookie里前端读取这个值放到自定义Header里服务端比对Cookie值和Header值是否一致。好处是无状态适合分布式服务。但我不太推荐新手直接用。原因在于实现细节太容易出错。如果Token所在的Cookie设置了HttpOnly前端脚本读不到就无法放进Header。如果把Cookie的Domain设置得过宽比如作用于整个主域名某个子域被攻击者控制后就能向下写Cookie攻击者可以生成一对匹配的Cookie值和Header值从而绕过校验。再加上SameSite配置、跨域CORS配置、代理改写等问题很容易翻车。如果团队必须用双提交Cookie至少要做到Cookie设置合理的Domain和Path范围、配合SameSite、服务端做精确的源校验、并在上线前用攻击页面完整回归。如果有选择用框架的Session方案会更稳。4.4 快速排查清单我把排查项整理成表方便对照检查。检查点操作方法通过标准是否开启全局CSRF查看框架配置或中间件所有写接口默认启用Token是否与会话绑定打印Session中的Token和请求中的Token能正确对应登录态Cookie的SameSite打开开发者工具查看Cookie属性设置为Lax或StrictOrigin白名单是否精确构造跨站请求并篡改Origin非白名单来源被拒绝写接口是否使用安全方法审查路由定义写操作只用POST/PUT/DELETEGET接口是否有副作用逐一检查GET路由GET接口不产生状态变更前端是否暴露Token检查脚本可访问的存储位置无XSS时Token不可被读取这张表不是一次性写完就结束每次上线新功能和改动安全配置时都要重新过一遍。5. 个人实操中的一点体会讲到这里我想分享一次真实的排查经历。有次在内部安全评估中我发现一个后台管理系统的“修改绑定手机号”接口只校验了登录态。当时我构造了一个隐藏表单嵌在一张看起来像“抽奖活动”的页面里只要管理员在另一个标签页访问过那个页面请求就会自动带着管理员的会话Cookie发出去。结果演示很成功管理员账号的绑定手机号被改成了一个测试号码。项目负责人特别惊讶地问我“我们用了HTTPS怎么可能还有这种漏洞”这是个很典型的误区。HTTPS保证的是传输过程中数据不被窃听或篡改它解决不了“服务器把恶意请求当成正常请求”的问题。CSRF发生在业务逻辑层不在传输层。从那以后我对CSRF的态度变成本能式防御所有改变状态的请求默认都要过CSRF校验能用框架就用框架不自己重复造复杂轮子。调试时先在浏览器Network面板里确认请求头是否带预期凭证上线前用攻击页面快速回归一遍。这套流程帮我避开了不少风险。最后分享一个实用的小技巧。排查SameSite和Cookie携带问题时我习惯用无痕窗口配合浏览器的开发者工具在Network面板里可以直接看到每个请求的Cookie、请求头和响应头。重点查看目标站种下的Cookie的Domain、SameSite、Secure三个属性和预期对比一遍基本能解决八成“为什么没带Cookie”的疑问。安全修复这件事很多时候不是思路不够而是细节没有查透。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑