- 发布于
Web 中的身份认证 4——CSRF
AI 辅助翻译自英文阅读英文原文
- 作者

- 姓名
- Garfield Zhu
- @_AlohaYo_
@Author: Garfield Zhu
Web 应用身份认证(4)——CSRF
我原本没计划写这页,它只是本系列第一篇文章中的一个小节。但实践更多之后,我发现其中遗漏了太多(可能并非最佳、却很必要的)网站安全实践。
因此,我想展开讨论 CSRF 这个主题。
什么是 CSRF
CSRF 代表跨站请求伪造(Cross-Site Request Forgery)。这个名称相当直白地描述了攻击:恶意网站触发向你的网站发出的请求,并伪装成用户主动发起。
CSRF 成为可能的关键在于浏览器处理 Cookie 的方式。用户登录网站后,会话 Cookie 存在浏览器中。任何请求发往你的域名时——即使请求完全由其他网站发起——浏览器也会自动附加该 Cookie。服务器看到有效会话,就会接受请求。
攻击者不需要用户凭据,只需让用户在已登录某处的情况下访问他们的页面。
攻击如何运作
举个具体例子。假设银行网站有端点:
POST /transfer
Body: { to: "account_id", amount: 1000 }
认证通过浏览器自动发送的会话 Cookie 处理。
攻击者创建带有以下隐藏表单的页面:
<form action="https://yourbank.com/transfer" method="POST" id="evil">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById("evil").submit()</script>
如果你登录银行时访问该页面,表单会自动提交,浏览器会随请求发送会话 Cookie。服务器看到来自已认证用户的合法请求,就会处理转账。
攻击者从未接触你的凭据,也不需要接触。
GET 请求也能这样工作,例如 <img src="https://yourbank.com/transfer?to=attacker&amount=10000" /> 只需加载图片标签就能触发 GET 端点。
防御 1:CSRF 令牌
经典防御方式:服务器生成随机且不可预测的令牌,将其嵌入表单(或提供给 JavaScript)。每次改变状态的请求都要求在请求体或自定义标头中携带令牌,而不是放在 Cookie 中。
由于攻击者页面属于不同源,无法读取你的页面内容(同源策略会阻止它),因此不知道该在伪造请求中放入什么令牌。
令牌必须:
- 随机且不可预测(使用密码学安全的生成器)
- 与用户会话绑定
- 每个会话不同,最好每个请求都不同
对于传统服务端渲染表单,可作为隐藏字段嵌入:
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<!-- rest of form -->
</form>
对于 JavaScript 使用的 API,通常将令牌放在 JavaScript 可读取的 Cookie 中(因此不能设置 HttpOnly),由 JS 读取后作为 X-CSRF-Token 等标头加入请求。服务器检查标头值是否与 Cookie 值匹配。
这就是 Double Submit Cookie 模式,稍后会再说明。
防御 2:SameSite Cookie 属性
SameSite Cookie 属性可能是现代防御 CSRF 最简洁的方式。它告诉浏览器不要在跨站请求中发送 Cookie。
Set-Cookie: session_id=abc123; SameSite=Strict; HttpOnly; Secure
它有三个值:
Strict:跨站请求一律不发送 Cookie,包括从其他网站点击链接进入你的网站;用户到达后不会保持登录。对多数应用而言过于严格。Lax:跨站子请求(图片、iframe、AJAX)不发送,但用户通过链接直接导航到网站时发送。它是浏览器近几年的默认值,对多数会话 Cookie 都合理。None:所有跨站请求都发送,且必须配合Secure。只有明确需要跨站 Cookie 时才使用,例如嵌入式组件、跨域认证流程。
对大多数应用,会话 Cookie 使用 Lax 是很好的默认选择。它能防护上述 CSRF 攻击,同时不破坏普通链接导航。即使主会话 Cookie 使用 Lax,与银行转账等高度敏感操作绑定的独立 Cookie 也值得使用 Strict。
防御 3:Double Submit Cookie
前面提到的模式思路如下:
- 在 Cookie 中设置随机 CSRF 令牌(可由 JavaScript 读取,因此不能设置
HttpOnly) - 每次改变状态的请求中,JavaScript 读取 Cookie 并将令牌加入请求标头
- 服务器检查标头值是否与 Cookie 值匹配
// On the client side
const csrfToken = getCookie('csrf_token')
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to: 'account', amount: 100 }),
})
其有效原因是:不同源的攻击者可以让浏览器带 Cookie 发出请求,但无法读取 Cookie 值(同源策略),因此无法设置匹配的标头。
需要注意:如果应用存在 XSS 漏洞,攻击者可通过 JavaScript 读取 Cookie,完全绕过这一防御。CSRF 令牌不能替代修复 XSS。
与 CORS 的关系
CORS(跨源资源共享)与 CSRF 是相关但不同的问题。
CORS 控制浏览器是否允许跨源脚本读取响应,并不能阻止请求发送。错误配置的 CORS 策略本身不会导致 CSRF,但意味着恶意脚本可以读取服务器响应,可能造成其他问题。
Access-Control-Allow-Origin: * 与 Access-Control-Allow-Credentials: true 的宽松配置实际上无效(浏览器会拒绝),但允许任意来源携带凭据会破坏 Double Submit Cookie 模式赖以成立的假设。
严格的 CORS 策略很重要,却不能取代 CSRF 防御,两者都应配置。
实用检查清单
- 在会话 Cookie 上使用
SameSite=Lax或SameSite=Strict - 对改变状态的端点(POST、PUT、DELETE)要求请求中的 CSRF 令牌,或验证
Origin/Referer标头 - 对传统服务端渲染表单,将 CSRF 令牌嵌入隐藏字段
- 对 JavaScript 使用的 API,使用 Double Submit Cookie 模式或同步令牌
- 不要只依赖
Content-Type检查——application/x-www-form-urlencoded和multipart/form-data属于简单请求,会绕过 CORS 预检 - 在服务端额外检查
Origin标头;当Origin与你的域名不匹配时拒绝请求
参考
- 以 OWASP Top 10 Vulnerabilities 为 Web 安全目标参考。
- 阅读 OWASP Cheetsheet 获取更多建议。
- OWASP CSRF Cheat Sheet
- MDN - SameSite cookies