服务端阅读 05月30日 22:17
什么是 CSRF 攻击?它如何工作,又该怎么防护?
CSRF(跨站请求伪造)不是偷 Cookie,而是借浏览器自动带 Cookie 的机制,冒充用户发起操作。典型流程是:用户已经登录 bank.example,浏览器里有登录 Cookie;随后用户打开攻击者页面,页面自动提交一个转账、改邮箱或删除数据的请求;请求发到 bank.example 时,浏览器会按域名自动带上 Cookie。服务器如果只看 Cookie 判断“用户已登录”,却不校验这次操作是不是从本站页面主动发起,就会把伪造请求当成真实操作。CSRF 成立通常要同时满足几个条件:用户处于登录状态;目标站使用 Cookie、Session 这类浏览器会自动携带的凭证;接口会改变状态;服务端没有校验 CSRF Token、Origin、Referer 或 SameSite 等额外信号。少了其中任何一环,攻击难度都会明显上升。追问CSRF 和普通跨域请求有什么关系?很多人以为浏览器有同源策略,所以 CSRF 发不出去,这是误解。同源策略主要限制攻击者读取跨站响应,不能阻止浏览器发送表单、图片、脚本、跳转这类请求。CSRF 往往不需要读响应,只要服务器执行了“改状态”的动作,攻击就已经成功。CORS 也是类似道理:它控制前端脚本能不能读取响应,不等于所有跨站请求都被拦截。尤其是普通表单提交、图片加载、顶级导航,本来就不依赖 CORS 成功读取响应。GET、POST、JSON API 都会被 CSRF 打中吗?GET 如果只做查询,风险相对小;但如果 GET 做删除、改状态、触发任务,就非常危险,因为 <img>、<script>、<a> 都能轻易触发 GET。POST 表单同样能被恶意页面自动提交,所以“改成 POST”不是完整防护。JSON API 的攻击门槛高一点,因为 application/json 和自定义请求头通常会触发 CORS 预检。但如果服务端错误放开 CORS,或者接口同时兼容表单格式,仍然可能被利用。接口设计上应坚持:读操作和写操作分离,写操作必须带额外校验。CSRF Token 为什么有效?CSRF Token 的作用是证明“请求来自服务端渲染或授权过的本站页面”。服务端生成一个不可预测的随机值,放在页面、meta 标签或接口返回里,前端提交写请求时把它放进隐藏字段或 X-CSRF-Token 请求头。攻击者页面不能读取目标站页面内容,也就拿不到这个随机值。Token 不能随便设计:不要放 URL 里,避免进入日志和 Referer;要绑定用户会话或登录态;过期策略要合理;高风险操作可以使用一次性 Token。Token 校验失败时,应该返回明确的 403,并记录来源、用户、接口和 request id,方便排查误杀。SameSite、Origin、Referer 应该怎么配合?现代防护通常是组合拳。Cookie 设置 SameSite=Lax 可以挡住大量跨站 POST 和 iframe 场景;SameSite=Strict 更安全,但会影响外部链接跳转后的登录体验;必须跨站携带 Cookie 时才用 SameSite=None; Secure。服务端还可以校验 Origin,没有 Origin 时再看 Referer。这两者适合做来源判断和日志审计,但不建议作为唯一防线,因为隐私策略、代理、旧浏览器或特殊跳转可能让头部缺失。更稳的策略是:SameSite 降低默认风险,Token 验证操作意图,Origin/Referer 做辅助拦截。XSS 会不会绕过 CSRF 防护?会。只要攻击者能在你的页面里执行脚本,就可能读取非 HttpOnly 的 CSRF Token,或者直接在同源上下文里调用接口。也就是说,CSRF Token 不能替代 XSS 防护。所以安全设计要分层:Cookie 使用 HttpOnly、Secure、SameSite;页面输出要做转义和 CSP;写接口校验 Token 和来源;高风险操作再加二次确认、幂等号或重新输入密码。不要指望一个机制解决所有问题。示例下面是一个最简单的 CSRF 攻击示例。用户只要打开攻击页面,浏览器就会向目标站发起请求:<form action="https://bank.example/transfer" method="POST"> <input name="to" value="attacker"> <input name="amount" value="1000"></form><script>document.forms[0].submit()</script>服务端防护可以这样做:app.post('/transfer', requireLogin, csrfCheck, async (req, res) => { await transfer(req.user.id, req.body.to, req.body.amount); res.json({ ok: true });});function csrfCheck(req, res, next) { const token = req.get('x-csrf-token') || req.body.csrf_token; if (!token || token !== req.session.csrfToken) return res.sendStatus(403); next();}这段逻辑的重点不是代码长短,而是把“用户已登录”和“用户确实从本站页面发起操作”分开验证。CSRF 正是利用了很多系统只验证前者、忽略后者的漏洞。