技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTETC

前端安全系列(二):如何防止 CSRF 攻击?

所属馆藏
Other
文件格式
Markdown
原始路径
Other/12-前端安全系列(二):如何防止CSRF攻击?
本文目录15 个章节
  1. 一、一个历史故事说明 CSRF 的影响
  2. 二、什么是 CSRF
  3. 三、常见的攻击请求类型
  4. 四、CSRF 与同源策略的关系
  5. 五、优先级最高的防护策略
  6. 六、校验请求来源
  7. 七、SameSite Cookie
  8. 八、验证码、重新认证和 WebAuthn
  9. 九、XSS 会绕过 CSRF 防护
  10. 十、分布式系统中的 Token
  11. 十一、防止网站成为攻击来源
  12. 十二、如何测试 CSRF
  13. 十三、历史案例如何阅读
  14. 十四、上线检查清单
  15. 参考资料

前端安全系列(二):如何防止 CSRF 攻击?

Category(分类): Other Status: 持续更新

原文参考:美团技术团队:前端安全系列(二)——如何防止 CSRF 攻击?

CSRF(Cross-Site Request Forgery,跨站请求伪造)利用的是浏览器的“隐式凭证”机制:浏览器可能自动携带目标站点的 Cookie、HTTP 认证信息或客户端证书,但目标站点不一定能知道这个请求是不是用户主动发起的。

本文中的请求只用于解释攻击原理。安全验证必须在自己拥有或明确授权的测试环境中进行。

一、一个历史故事说明 CSRF 的影响

原文引用过 2007 年 Gmail 过滤器被恶意修改的历史案例:用户登录 Gmail 后访问了一个第三方页面,页面诱导浏览器向 Gmail 发起改变设置的请求,服务端只检查登录 Cookie,于是把请求当成了用户操作。

这个案例现在已经修复,不能把原文中的 Gmail 地址或表单复制出来测试。但它说明了几个重要事实:

  • CSRF 不一定需要读取 Cookie;
  • 攻击者可能只需要让浏览器发出一个请求;
  • 影响范围取决于目标接口和用户权限,可能包括改密、转账、改邮箱、添加管理员或修改邮件规则;
  • “用户已经登录”与“用户批准了这次请求”是两件不同的事。

二、什么是 CSRF

典型 CSRF 流程如下:

  1. 用户登录受信任站点 A,浏览器保存 A 的会话 Cookie;
  2. 用户没有退出 A,又访问攻击者控制的站点 B;
  3. B 通过图片、表单、链接或其他方式向 A 发起请求;
  4. 浏览器根据 Cookie 属性决定是否自动携带 A 的 Cookie;
  5. A 只验证 Cookie 有效,没有验证请求是否来自用户主动操作;
  6. A 以用户身份执行了攻击者指定的改变状态操作。

跨源请求的响应通常不能被 B 的脚本读取,但 CSRF 通常不需要读取响应。浏览器能否发出请求、目标站点能否识别并拒绝伪造请求,才是关键。

1. CSRF 成功的常见条件

通常需要同时满足:

  • 目标站点使用 Cookie、HTTP Basic Auth 或其他会自动附加的凭证;
  • 目标接口会改变服务端状态;
  • 请求不需要攻击者无法获得的额外证明,例如 CSRF Token 或自定义请求头;
  • 用户已登录且拥有执行该操作的权限;
  • 浏览器的 Cookie 策略允许该凭证随跨站请求发送。

如果认证令牌只放在 Authorization 请求头,并且第三方页面无法设置该头,传统 Cookie 型 CSRF 风险会降低;但仍需根据 CORS、登录流程、简单请求和其他认证机制进行评估。

三、常见的攻击请求类型

1. GET 类型

如果错误地使用 GET 完成改变状态的操作,第三方页面可能通过图片或链接触发请求:

<img src="https://bank.example/withdraw?amount=10000&amp;for=attacker" alt="">

这就是为什么 HTTP 语义要求 GET、HEAD、OPTIONS 等安全方法不要改变服务器状态。把转账、删除、改密等接口改成 POST 是必要的,但仅改成 POST 仍然不够

2. POST 类型

第三方页面可以构造普通 HTML 表单提交 POST:

<form action="https://bank.example/withdraw" method="post">
  <input type="hidden" name="amount" value="10000">
  <input type="hidden" name="for" value="attacker">
</form>

表单可以在用户交互后提交,也可能由脚本自动提交。服务端不能把“请求方法是 POST”当作 CSRF Token。

3. 链接和顶级导航

链接、重定向、表单和嵌入资源都可能触发跨站请求。现代浏览器的 SameSite 策略会阻止很多子资源请求携带 Lax Cookie,但不能覆盖所有浏览器、Cookie 配置和业务流程。安全防护必须由服务端完成。

4. CORS 不是 CSRF 防护

CORS 主要决定跨源 JavaScript 能否读取响应。简单的跨源请求可能不触发预检,仍然可能到达服务端。因此不能认为“接口没有开放 CORS,就不会被 CSRF”。

四、CSRF 与同源策略的关系

同源策略一般阻止 B 读取 A 的 Cookie、DOM 和响应内容,但并不阻止所有跨源请求。CSRF 正是利用了“请求可以发出,但响应不能被读取”的差异。

还要区分:

  • origin(源):协议、主机和端口;
  • site(站点):用于 Cookie SameSite 判断的更宽泛概念。

同站的不同子域不一定同源。如果把会话 Cookie 的 Domain 设置得过宽,某个不安全的兄弟子域可能影响整个站点的 Cookie 安全边界。

五、优先级最高的防护策略

1. 使用框架内置 CSRF 防护

先检查后端框架是否提供 CSRF 中间件、Token 生成和校验。成熟实现通常能减少 Token 失效、并发页面、错误处理和异常日志方面的遗漏。

2. 不让 GET 改变状态

所有删除、修改、转账、下单、改密、添加权限等操作都不应通过 GET、图片或链接完成。使用 POST、PUT、PATCH 或 DELETE 后,仍要校验 Token、Origin 或其他请求真实性信号。

3. 使用 Synchronizer Token

有服务端会话时,可以把随机 Token 存在当前会话中,再输出到 HTML 或接口响应:

<form action="/profile/email" method="post">
  <input type="hidden" name="csrf_token" value="服务端生成的随机值">
  <input type="email" name="email">
  <button type="submit">保存</button>
</form>

AJAX 请求可以把 Token 放在自定义请求头中:

await fetch('/profile/email', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ email })
})

服务端应当:

  1. 使用密码学安全的随机数生成 Token;
  2. 将 Token 与用户会话绑定;
  3. 校验缺失、格式错误和不匹配的 Token;
  4. 记录异常请求并返回合适的错误;
  5. 不把 Token 放入 URL,避免进入历史记录、日志、代理和 Referer;
  6. 根据框架设计决定按会话还是按请求更新,不能为了“每次都换”破坏多标签页和后退按钮。

Node.js 生成会话 Token 的示意:

import { randomBytes } from 'node:crypto'

const csrfToken = randomBytes(32).toString('base64url')

Token 不是密码,不需要靠“加密后就安全”。核心是不可预测、保密、与当前会话绑定以及服务端校验。

无服务端会话或希望使用无状态方案时,可以把随机值放入 Cookie,同时要求客户端把它显式放入请求头或请求体,服务端比较两份值。

朴素的“Cookie 值等于请求参数值”方案可能受到 Cookie 注入、子域控制或明文 HTTP Cookie 注入影响。新系统优先考虑与会话绑定的签名 Double Submit Cookie

csrf_token = HMAC(server_secret, session_binding + random_value) + "." + random_value

服务端重新计算 HMAC 并使用常量时间比较。不要只对一个不绑定会话的随机 Cookie 做简单相等比较,也不要把会话 ID本身放进客户端 Token。

CSRF Token Cookie 需要被前端读取时,通常不能设置 HttpOnly;真正的会话 Cookie 仍应设置 SecureHttpOnly 和适当的 SameSite

六、校验请求来源

1. Origin

对改变状态的请求,服务端可以精确校验 Origin

Origin: https://app.example.com

只允许经过明确配置的协议、主机和端口,不要用字符串包含关系判断信任。例如,https://example.com.attacker.test 不是 https://example.com

Origin 不是所有请求都一定存在,可能受到请求类型、重定向、浏览器兼容性或隐私处理影响。缺失时应按接口风险选择拒绝、回退到 Referer 或要求 Token,不能无条件放行 Origin 缺失。

2. Referer

Referer 可以作为兼容性回退或监控信号,但不应作为唯一防线:

  • Referrer-Policy、隐私设置、HTTPS 降级和浏览器行为可能使它缺失或被裁剪;
  • 非浏览器客户端可以伪造请求头;
  • UGC、本域 XSS 或被控制的同站子域可能使来源校验失去意义。

Referrer-Policy: strict-origin-when-cross-origin 可以减少路径和查询参数泄露,但它不是 CSRF Token,也不会主动阻止请求。

3. Fetch Metadata

现代浏览器可能发送:

  • Sec-Fetch-Sitesame-originsame-sitecross-sitenone
  • Sec-Fetch-Mode:请求模式;
  • Sec-Fetch-Dest:请求目标类型;
  • Sec-Fetch-User:是否由用户导航触发。

服务端可以把 Sec-Fetch-Site: cross-site 的非安全方法作为明显的 CSRF 请求拒绝,并对缺失这些头的旧客户端使用 Token 或 Origin/Referer 回退。上线前应先记录将被阻断的请求,确认不会误伤支付、登录回调、第三方集成等合法流程。

SameSite 通过限制跨站请求发送 Cookie 来降低 CSRF 风险,但它是纵深防御,不应替代 Token:

属性主要行为注意事项
Strict跨站场景基本不发送 Cookie防护强,但从外部链接进入可能没有登录状态
Lax同站发送,跨站顶级安全导航通常允许发送跨站图片、脚本和 iframe 等子资源通常不带 Cookie;不能保护错误的 GET 状态变更
None跨站也允许发送 Cookie必须同时设置 Secure,需要额外 CSRF 防护

现代浏览器对未设置 SameSite 的 Cookie 通常采用接近 Lax 的默认策略,但不同浏览器、第三方 Cookie 政策和最近创建 Cookie 的宽松例外可能不同。不要依赖默认值。

会话 Cookie 示例:

Set-Cookie: __Host-session=随机会话值; Path=/; Secure; HttpOnly; SameSite=Lax

__Host- Cookie 不能设置 Domain,有助于缩小子域影响范围。SameSite 判断的是站点,不等于同源;如果同一注册域下存在不受信任的子域,应更谨慎地设置 Cookie 范围和信任关系。

八、验证码、重新认证和 WebAuthn

转账、修改密码、删除账号、添加管理员等高风险操作可以要求:

  • 重新输入密码;
  • 验证码或明确的用户确认;
  • WebAuthn/Passkey 等抗钓鱼认证;
  • 交易签名、设备确认或风控策略。

这些是高风险操作的额外保护,不适合用来替代普通请求的 Token。每个点击都弹验证码会严重影响体验,也不能修复 XSS 或越权。

九、XSS 会绕过 CSRF 防护

CSRF Token 依赖页面中的同源 JavaScript 读取或提交。如果站点存在 XSS,恶意脚本可以在同源上下文读取 Token、调用接口或直接操作页面,因此:

  • CSRF 和 XSS 是不同漏洞,必须分别修复;
  • HttpOnly 可以保护某些 Cookie 不被读取,但不能阻止同源 XSS 发起请求;
  • CSP、输出编码、HTML Sanitizer 和 Trusted Types 是防止 XSS 绕过 CSRF 防护的配套措施,不是 CSRF Token 的替代品。

十、分布式系统中的 Token

早期文章常把 Token 存在单机 Session 中。今天的应用可能有多个实例、多个机房和 CDN/网关层,可以选择:

  • 使用共享 Session 存储,例如 Redis,并处理高可用和过期;
  • 使用签名、与会话绑定的无状态 Token;
  • 由网关统一注入和校验,但业务服务仍要明确哪些接口需要保护;
  • 在缓存层正确设置 Vary,避免把带用户状态的响应错误共享。

不要为了“无状态”把用户 ID、时间戳和秘密直接拼起来再 Base64。可验证的签名、密钥保护、过期和会话绑定比自创“加密 Token”重要。

十一、防止网站成为攻击来源

攻击者可能从自己的站点发起 CSRF,也可能利用被攻陷的论坛、文件上传、评论或本域 UGC 页面作为来源。网站应:

  • 严格限制上传文件类型、大小、内容和下载响应头;
  • 对用户 HTML、Markdown、SVG 和富文本做安全清洗;
  • 设置 X-Content-Type-Options: nosniff,防止 MIME 类型猜测;
  • 不把用户填写的图片或链接直接当作可信资源;
  • 对外部链接使用 rel="noopener noreferrer" 等合适属性并评估跳转风险;
  • 防止本域 XSS,因为同源内容可以绕过单纯的跨域来源判断。

nosniff 和上传校验不能替代 CSRF Token,它们解决的是内容执行和 MIME 混淆问题。

十二、如何测试 CSRF

只在授权测试环境中验证,建议覆盖:

  1. 每个改变状态的接口是否错误地接受 GET;
  2. 缺失 Token、Token 为空、Token 错误和 Token 属于其他会话时是否返回拒绝;
  3. Origin 为不可信来源、缺失或 null 时的处理;
  4. Referer 被裁剪或缺失时是否有安全回退;
  5. Cookie 的 SecureHttpOnlySameSiteDomainPath 是否符合预期;
  6. Sec-Fetch-Site: cross-site 的非安全请求是否被正确处理;
  7. CORS 允许的来源、凭证和预检规则是否精确;
  8. 多标签页、后退按钮、登录过期和重试是否不会误伤合法用户。

可以参考 OWASP Web Security Testing GuideOWASP ZAP。原文提到的 CSRFTester 属于较早工具,使用前应确认维护状态;更重要的是先建立接口清单并由人工复核。不要在生产环境生成会自动转账、删数据或修改账号的攻击页面。

监控方面,可以在网关或服务端记录 Token 失败、Origin 不匹配、跨站非安全方法和异常来源,但要注意脱敏,不能把完整 Token 写入日志。

十三、历史案例如何阅读

WordPress、YouTube 和 Gmail 的旧漏洞案例说明,CSRF 影响取决于接口权限和业务动作,不能因为“只是一个表单”就低估风险。它们的具体版本和接口已经变化,今天不应直接复现原文中的旧利用代码。

阅读历史案例时应关注:

  • 接口是否错误地使用 GET;
  • 是否只验证 Cookie 而没有验证请求意图;
  • 是否缺少 Token、来源校验和 SameSite;
  • XSS 或子域控制是否可以进一步绕过防护;
  • 修复后是否补充了测试和监控。

十四、上线检查清单

  • 改变状态的接口不使用 GET、HEAD 或其他安全方法;
  • 框架内置 CSRF 防护已启用并覆盖所有相关路由;
  • 有会话的应用使用不可预测、与会话绑定的 CSRF Token;
  • 无状态方案使用签名 Double Submit Cookie,而不是简单相等比较;
  • Token 不出现在 URL、日志、Referer 或缓存键中;
  • 精确校验 Origin,必要时使用安全的 Referer 回退;
  • 评估 Sec-Fetch-Site,并为不支持的客户端保留回退方案;
  • 会话 Cookie 使用 SecureHttpOnly、合适的 SameSite 和最小 Domain;
  • CORS 只允许明确来源,带凭证时不使用 Access-Control-Allow-Origin: *
  • 高风险操作增加重新认证、确认或 WebAuthn;
  • XSS、上传、UGC、子域和供应链风险已分别处理;
  • 测试覆盖 Token 缺失/错误、来源异常、重定向、缓存和多标签页场景。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS