首页 / 资讯中心 / 文章详情

CSRF跨站请求伪造全解析:原理、攻击场景与六种防护方案

CSRF跨站请求伪造全解析:原理、攻击场景与六种防护方案 ★ FEATURED ARTICLE
CSRF 这个坑几乎每个写 Web 应用的人都踩过但很多人直到被安全扫描报告砸到脸上才真正搞明白它到底是个什么东西。我见过不少团队项目都快上线了才发现接口能被第三方网站随意调用然后连夜赶工补防护。这篇文章就把 CSRF 彻底讲透它是什么、攻击者怎么利用它、以及主流的几种解决方式各自适用什么场景该避哪些坑。如果你是在校学生、刚转行做后端或前端的开发者又或者是项目中负责接口设计的技术负责人这篇文章都值得仔细看一遍。看懂 CSRF 的原理你才能在设计接口时从根源上规避风险而不是等出了事故再做补救。1. 什么是 CSRF它到底在攻击什么CSRF 全称是 Cross-Site Request Forgery中文通常叫跨站请求伪造。它攻击的不是用户的电脑也不是用户的密码而是浏览器对已登录网站的信任关系。想象一个场景你在某银行网站登录了账户浏览器里存着有效的会话 Cookie。然后你又打开了另一个恶意网页这个网页里藏着一行隐藏的请求指向银行网站的转账接口。浏览器会怎么做它会带着你之前存好的 Cookie老老实实地把这个请求发出去。服务器一看Cookie 有效请求就成功了。整个过程里攻击者根本没有拿到你的任何凭据他只是利用了浏览器“自动携带 Cookie”这个特性伪造了你的身份。这就是 CSRF 的本质利用浏览器对用户身份认证信息的自动携带机制以用户的名义向目标网站发起攻击者预设好的请求。1.1 攻击成立必须满足的三个前置条件一个 CSRF 攻击要真正生效需要同时满足这几个条件缺一个都打不穿目标网站依赖 Cookie 进行身份认证。现在很多前后端分离的项目用的是 Token 存在 localStorage 里那 CSRF 的攻击面就会小很多但并不是完全免疫。受害者在目标网站处于已登录状态。这是前提中的前提如果你只打开页面但没登录攻击请求也没法通过认证校验。目标接口允许跨域请求或者请求根本不受同源策略约束。这是个关键点后面我会细说。很多人容易把 CSRF 和 XSS 搞混。XSS 是通过注入恶意脚本直接偷走用户的 Cookie 或者操作页面而 CSRF 是凭空伪造请求攻击者从头到尾都接触不到用户的 Cookie 和页面内容。简单说XSS 偷的是钥匙CSRF 借的是你插在锁孔里没拔出来的钥匙。1.2 两类攻击形态GET 型与 POST 型CSRF 攻击最常见的有两种形态。GET 型很好理解攻击者构造一个图片链接或者超链接当受害者打开页面时浏览器自动请求这个 URL。比如一个图片标签img srchttp://bank.com/transfer?toattackeramount10000只要用户浏览器的 Cookie 还在这个 GET 请求就会被发送出去。这类攻击成本极低有时候聊天软件里发个链接别人一点就中招。POST 型则需要借助表单自动提交。攻击者构造一个隐藏表单页面加载时用 JavaScript 自动form.submit()。这种方式可以模拟 POST 请求攻击成功率和隐蔽性都比 GET 型高。但 POST 型攻击依然受限于一个关键约束后面讲同源策略时我会重点展开。2. 攻击原理拆解浏览器、Cookie 与同源策略的三角关系要真正理解 CSRF你必须明白浏览器在这个攻击链里扮演的角色以及它为什么会被“骗”到。2.1 浏览器自动携带身份信息的机制HTTP 协议本身是无状态的所以现代 Web 应用普遍用 Cookie 来维持会话状态。你登录后服务器返回一个会话 ID 存在浏览器之后每次请求同一个域名浏览器都会自动带上这个 Cookie。问题出在“自动”这两个字上。浏览器在发送请求时只判断目标域名是否匹配而不会判断这个请求是从哪个页面发起的。你对 A 网站发的请求浏览器会带上 A 网站的 Cookie哪怕这个请求是从一个恶意的 B 网站页面里发出来的浏览器依然会照带不误。这里就引出了保护 Web 安全的第一道防线同源策略。同源策略规定浏览器里运行的脚本只能访问同源协议、域名、端口一致的资源。它能限制 JavaScript读取跨域请求的响应但不能限制浏览器发送跨域请求。也就是说B 网站的脚本虽然读不到 A 网站的响应但它完全可以让浏览器向 A 网站发出一个请求——Cookie 该带还是会带。读懂这两者的区别你就能明白 CSRF 为什么能绕过同源策略攻击者要的不是服务器的响应他要的是让请求在服务器端成功执行。2.2 从一次模拟攻击看完整请求链我用一个简化的场景把攻击链路串起来。受害者已登录某在线购物平台账户里有余额并且有“一键下单”的功能接口是POST /order/create。攻击者构造了一个类似这样的恶意表单form actionhttp://shopping.com/order/create methodPOST input typehidden nameproductId value8888 / input typehidden namequantity value100 / /form script document.forms[0].submit(); /script受害者在浏览器里打开这个恶意页面表单自动提交请求发出浏览器自动附带了购物平台的会话 Cookie。购物平台的服务器收到请求校验通过订单创建成功。整个过程里受害者完全无感知攻击者也没有读取过任何不属于他的数据。这条链路里每一步都是浏览器和 Web 协议的正常工作方式攻击者没有利用任何系统漏洞他用的全部是“合法”能力。这就是 CSRF 难防的另一个原因它不像 SQL 注入那样有明显的攻击特征在服务器日志里看到的不过是一次正常的订单请求。2.3 为什么说 Cookie 的 SameSite 属性是天然的拦截器很多老开发在排查 CSRF 问题时第一反应就是加 Token但其实早在浏览器层面就有一道现成的防线Cookie 的SameSite属性。SameSite有三个取值Strict完全禁止第三方携带。所有跨站请求都不会带这个 Cookie。Lax跨站导航类请求比如从外链点进你的网站会带其他跨站请求不带。None无限制但必须配合Secure属性也就是只在 HTTPS 下生效。主流现代浏览器默认的SameSite值已经是Lax这意味着绝大多数跨站 POST 请求都不会携带 CookieCSRF 的载体直接被掐断了一大半。但这里有个很容易被忽略的坑很多同源判定是“站”而不是“源”。比如www.a.com和help.a.com在 Cookie 的 SameSite 判断里被认为是同一个站点请求之间可以携带 Cookie但严格按源的协议、域名、端口来说它们是两个源。我见过不少团队前端资源放在static.a.comAPI 放在api.a.com后端设置的 Cookie 域名写成了.a.com结果 SameSite 的防护形同虚设。所以在设计 Cookie 策略时主域名下两个子域之间的请求同样要纳入 CSRF 防护范围。3. 解决 CSRF 的六种主流方案怎么选怎么配目前业界防 CSRF 的手段已经相当成熟我按推荐程度从高到低逐个说明。注意这些方案不是互相排斥的实际项目中往往需要组合使用。3.1 方案一CSRF Token最经典也最可靠CSRF Token 的思路很简单服务器生成一个随机且不可预测的 Token下发给客户端客户端在发起敏感请求时必须携带这个 Token服务器校验 Token 合法才放行。关键细节在于“怎么下发”和“怎么携带”。我先说代码层面的标准做法。后端的逻辑通常长这样以 Java 为例思路通用PHP 和 Node 都适用// 生成 Token会话级随机字符串不重复且与当前用户会话绑定 String csrfToken UUID.randomUUID().toString(); session.setAttribute(csrfToken, csrfToken);然后前端渲染页面或者接口返回时把 Token 交给客户端。前端发起请求时通过自定义请求头携带// axios 示例在请求拦截器里注入 CSRF Token axios.interceptors.request.use(config { const token document.querySelector(meta[namecsrf-token]).getAttribute(content); if (token) { config.headers[X-CSRF-Token] token; } return config; });后端校验时对比请求头里的 Token 与会话里存的 Token 是否一致String requestToken request.getHeader(X-CSRF-Token); String sessionToken (String) request.getSession().getAttribute(csrfToken); if (sessionToken null || !MessageDigest.isEqual( sessionToken.getBytes(StandardCharsets.UTF_8), requestToken.getBytes(StandardCharsets.UTF_8) )) { throw new InvalidCsrfTokenException(CSRF token mismatch); }这里有几个细节必须注意。第一必须用MessageDigest.isEqual做时间恒定的比较防止时序侧信道攻击直接调用equals比较字符串是错误的做法。第二Token 必须与会话绑定不能用全局同一个值。第三校验失败时不要返回友好错误提示返回 403 就够了避免给攻击者提供探测信息。用自定义请求头携带 Token比放在表单隐藏字段里的优势在于跨域请求只有经过 CORS 预检允许自定义头才能带上这天然防住了一大部分 CSRF 攻击。但它的前提是前后端通信配套使用 CORS 策略如果你的项目里前端随便跨域、后端也没配 CORS这个优势就体现不出来。3.2 方案二校验 Origin 和 Referer 头成本最低的兜底方案最简单快捷的 CSRF 防护是校验请求的来源Origin请求头和Referer请求头。如果请求是从可信页面发起的这两个字段的域名应该和接口域名匹配或至少属于可信域名。POST /order/create Host: shopping.com Origin: http://shopping.com Referer: http://shopping.com/cart而后端要做的事就是读取请求头里的Origin和白名单比对不一致就拒绝。我常用的校验逻辑大致是这样String allowedOrigin http://shopping.com; String requestOrigin request.getHeader(Origin); if (requestOrigin ! null !allowedOrigin.equals(requestOrigin)) { return 403; }为什么这是兜底方案而不是核心方案因为Referer是可以在某些场景下缺失或被浏览器策略移除的。比如用户从地址栏直接输入网址、或使用某些隐私模式Referer可能为空Origin在部分 GET 请求中也可能不发送。所以我在项目里通常把 Origin/Referer 校验作为前置条件但它不能单独作为全部防护必须配合 CSRF Token 一起用。还有一点值得注意Origin校验要防的是域名白名单不要简单写死成接口域名。你的前端可能放在多个域名下那就把所有可信的前端域名都加进白名单。3.3 方案三SameSite Cookie 属性升级难度最低但依赖浏览器前面说过了SameSite 是浏览器层级的防护服务端只需要在设置 Cookie 时指定属性。Set-Cookie: sessionIdabc123; HttpOnly; Secure; SameSiteLax这里有个实际经验生产环境多数项目已经把 SameSite 设置成Lax它能挡住绝大多数跨站 POST 和 PUT 请求但对于 GET 型请求依然存在风险因为 Lax 模式允许顶层导航携带 Cookie。所以如果你的业务里有“用 GET 发敏感操作”的设计那我建议同时用 Token 兜底。线上排查时还要注意一个真实坑之前国内很多浏览器版本对标准支持不统一部分老版本将默认值当作None处理。如果通过设置SameSiteNone但没加SecureCookie 会被直接丢弃用户表现为“登录状态不稳定”“一会儿就掉线”。如果必须跨站携带 Cookie请确保已经全部启用 HTTPS否则不要碰None。3.4 方案四双重提交 Cookie适合无状态后端无状态架构流行之后会话不落服务端上面的“服务端存 Token 校验”就不好用了。双重提交 Cookie 的思路是客户端生成一个 Token放在 Cookie 里同时请求时把它放到自定义请求头里服务端只校验这两个值是否一致不需要服务端存储任何会话状态。这个方案的关键逻辑是攻击者无法在跨域场景下同时修改 Cookie 和请求头受同源策略保护所以只要两者一致请求基本就是可信的。但它有个明显的软肋如果目标站点存在子域 Cookie 注入漏洞攻击者可以在子域写入 Cookie同时就能获得正确的请求头值。因此这个方案适合安全要求不高、架构又必须无状态的项目核心业务不推荐单独使用。3.5 方案五二次校验机制防关键操作的核弹级保险对于修改密码、解绑手机、大额转账这类高危操作任何被动式的 CSRF 防护都可能不够。这时候要上主动校验要求用户再次输入密码、短信验证码或进行人脸验证。我在这里的建议是不要把这个当作方案而要当作底线。CSRF Token 可能因为前端框架处理不当而泄露或失效但二次校验的本质是“即使请求是伪造的攻击者也无法通过人机交互层的验证”这是不可绕过的。在实际业务里我强烈推荐把二次校验和 CSRF Token 叠加使用Token 负责挡住自动化请求二次校验负责挡住极端场景下的漏网之鱼。3.6 方案六自定义请求头作为唯一身份因子适合前后端分离前后端分离项目如果接口设计时约定必须带一个自定义请求头比如X-Requested-With: XMLHttpRequest那么天然挡住了一大批通过表单发起的跨站请求因为表单提交无法设置自定义请求头。但这个方法不解决所有问题因为攻击者可以用fetch等 JavaScript API 发起带自定义头的跨站请求而这时真正拦截他的是 CORS 预检。所以这个方案的正确用法是前端统一加头 后端配置严格的 CORS 白名单两者结合才能形成一个完整的闭环。// 前端统一加自定义头 fetch(/api/order/create, { method: POST, headers: { X-Requested-With: XMLHttpRequest, Content-Type: application/json }, body: JSON.stringify({ productId: 8888, quantity: 100 }) });4. 实操过程中的常见问题与排查技巧这一节我根据自己的踩坑经验把 CSRF 防护落地过程中最常见的问题和排查方法整理出来。4.1 上线后用户反馈“请求被拒”先查 Cookie 域和 SameSite 的交互最常见的问题是防护逻辑上线后一部分用户正常操作被拦截。排查时不要一头扎进 Token 校验代码里先看浏览器的 Cookie 面板确认登录会话 Cookie 是否在跨站请求场景下被浏览器自动丢弃。很多团队同时配置了SameSiteNone和Secure但开发环境是 HTTP导致 Cookie 在本地直接失效。正确做法是区分环境开发环境用Lax生产环境如果确实需要跨站带 Cookie再改成None并保证全站 HTTPS。4.2 Token 校验通过却仍然被攻击检查是否做到了会话绑定有些人以为只要“有 Token”就安全了但 Token 如果是全局唯一攻击者在自己的账户里也能拿到一个合法 Token然后用它构造攻击请求。服务器校验时发现 Token 合法照样放行。这就等于没有防护。排查方法很简单后端登录两个不同用户的账号看获取到的 Token 是否相同。如果相同说明 Token 没有和会话绑定这是高危缺陷必须改成每个会话独立生成。4.3 前端框架渲染导致 Token 丢失需要闭环追踪现在主流前端框架都是数据驱动渲染Token 不能只在某个页面初始化时获取一旦前端做了路由切换或局部刷新Token 可能就没被带上。我建议在请求拦截器里统一读取而不是散落在各业务代码里。另外如果 Token 放在 DOM 的 meta 标签中要确认 SSR服务端渲染阶段已正确注入用纯静态部署时Token 需要通过初始化接口下发再存到内存变量或某个安全的容器里。不要存 localStorage因为任何 XSS 都能直接读取它这等于把防线拆了。4.4 排查 CORS 配置导致 Origin 校验失效很多后端把 CORS 配置写成了Access-Control-Allow-Origin: *这等于告诉浏览器“所有域都可以读取响应”然后又在业务代码里校验 Origin。前者是响应头后者是业务逻辑两者是独立的互不兜底。拼写这层配置时我建议直接列出明确的来源域名白名单而不是用通配符。如果你发现 CORS 配置改了之后应用异常很大概率是预检请求OPTIONS没被正确响应。后端需要先处理OPTIONS请求返回允许的方法、请求头和来源然后再走业务逻辑。常见问题可能原因排查顺序用户操作被误拦截SameSite 与业务场景不匹配 / Cookie 域不对先看浏览器 Cookie 是否携带Token 存在但攻击仍成功Token 未与会话绑定 / Token 存在 localStorage抓包对比不同会话的 Token跨域请求响应拿不到CORS 响应头未配置或通配符导致预检失败浏览器开发者工具看 Network 的 OPTIONS登录状态掉线SameSiteNone 但未启用 HTTPS查 Cookie 的 Secure 标志4.5 一份够用的 CSRF 防护自查清单我分享一份自己常用的整改清单按重要性排序照着检查一遍基本能堵住大部分漏洞敏感操作只允许 POST/PUT/DELETE杜绝用 GET 改数据。服务端对所有敏感接口校验自定义请求头。业务级校验 CSRF Token且 Token 必须与会话绑定。Cookie 设置HttpOnly、Secure、SameSite按业务场景选择合适的 SameSite 值。CORS 白名单严格配置不使用*。高危操作增加二次校验。定期检查安全日志搜索异常的跨域来源请求。5. 不同技术栈的落地差异与工具选型解析既然不同项目用的技术不一样落地细节也有差异。我分别说一下主流技术栈的做法。5.1 后端框架自带防护模块怎么用Spring Security 对 CSRF 的防护是默认开启的它通过CsrfFilter拦截请求并默认把 Token 放在HttpSession中。http.csrf(csrf - csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers(/api/public/**) );把 Token 放在 Cookie 里并且 HttpOnly 为 False是为了让前端 JavaScript 能读取。但这里要格外小心Cookie 中的 Token 很容易被别的方式复用所以高安全要求的项目仍然建议用HttpSessionCsrfTokenRepository加自定义请求头的组合。对于 Node.js 技术栈经常用 csurf 这类中间件库用法和 Spring Security 类似核心都是一样的校验逻辑。选择库时重点看是否支持自定义 Token 存储和校验方式。5.2 网关层统一防护适合微服务架构微服务架构下给每个服务单独配 CSRF 防护既冗余又容易遗漏。更好的方案是在统一接入网关API 网关上做一次全面的 Token 校验然后下游服务默认信任网关传过来的身份标记。这里要说一个协作上的大坑网关做了 Token 校验下游服务还会再做一次而且双方用的 Token 不一致结果前端需要来回适配。所以团队内部必须明确分工要么全部在网关做要么网关放行后服务层继续做但 Token 的分发和校验规则必须全局一致。5.3 移动端、小程序场景的 CSRF 该怎么看移动端 App 和微信小程序没有浏览器自动携带 Cookie 的机制请求头都是代码手动添加的所以 CSRF 风险天然较低。但这不代表完全没风险如果 App 内部嵌了 WebView打开的是第三方网页WebView 里的请求仍可能携带 Cookie这时 CSRF 又会回来。所以团队在开发 App 内嵌页面时建议仍然按 Web 标准做 CSRF 防护不要因为“我们用的是 App”就跳过这套防护。6. 方案组合推荐从低配到高配的三层防护架构说了这么多真正落地时怎么组合最合理我按项目的安全等级给出三种推荐配置。低配版适合内部管理系统或低价值业务全部敏感接口拒绝 GET。校验 Origin 头。Cookie 设置SameSiteLax。中配版适合面向用户的常规业务低配版基础上加上 CSRF Token放在自定义请求头。CORS 严格白名单。高危操作加二次校验。高配版适合金融、支付、账号体系等核心业务中配版基础上Cookie 全站SameSiteStrict或根据场景动态切换。网关层统一拦截所有请求都需要通过 Token 校验后才会路由到下游。针对账号绑定、密码修改、核心交易等场景强制二次人机验证。建立安全监控实时分析异常来源请求。无论选哪种配置我在实际项目里坚持的一个原则是CSRF 防护不能只依赖单一技术手段因为每一种手段都有盲区。Token 怕 XSSSameSite 怕浏览器兼容Origin 校验怕策略配置错误只有组合起来才能形成有纵深的防御体系。从我个人的体会来说做完 CSRF 防护不是终点隔一段时间再照着自查清单复查一遍才是负责任的做法。因为技术债最难还的往往不是代码逻辑而是“当时配置过就以为永远有效”的侥幸心理。项目迭代了、前端域名换了、Cookie 策略改了任何一个环节都可能重新撕开 CSRF 的口子。如果你正在设计新项目的接口我建议从一开始就把这些防护纳入开发规范而不是等安全测试报告出来再补救。一套成熟的 CSRF 防护做下来工作量可能就多半天但能帮你省掉后面无数个提心吊胆的夜晚。
阅读完成 · 觉得有帮助?
咨询建站