为什么“已经登录”反而可能成为 CSRF 的条件?
CSRF(Cross-Site Request Forgery,跨站请求伪造)利用的不是攻击者知道了用户密码,而是浏览器可能在请求目标网站时自动携带用户已经拥有的登录凭据,例如 Cookie。
假设用户已经登录 example.com。此时如果另一个恶意页面能够诱导浏览器向 example.com 发起一个会修改数据的请求,而服务器只看到“请求带着有效登录 Cookie”就执行操作,就可能产生 CSRF 风险。
因此 CSRF 防护的核心不是再次证明“用户已登录”,而是确认:这个敏感请求确实来自网站认可的交互流程。
先看一个最简单的风险场景
假设某个网站使用 Cookie 保存登录会话,并存在修改邮箱的接口:
POST /account/email
Cookie: session=USER_SESSION
email=new@example.com
如果服务器只验证 session Cookie,而没有其他 CSRF 防护,攻击者可能尝试从第三方页面构造请求。浏览器是否会携带 Cookie,取决于 Cookie 属性、请求方式和浏览器规则。
现代浏览器的 SameSite 已经降低了很多传统 CSRF 场景的成功率,但它不应该成为唯一防线。
SameSite 是第一道重要防线
SameSite 是 Cookie 属性,用于限制跨站场景下 Cookie 的发送行为。常见值包括 Lax、Strict 和 None。
SameSite=Lax
Set-Cookie: session=…; Secure; HttpOnly; SameSite=Lax
Lax 通常能阻止许多跨站子资源请求或跨站表单 POST 携带该 Cookie,同时保留部分顶级导航场景的可用性。
对于很多普通网站的会话 Cookie,Lax 是兼顾兼容性与 CSRF 风险控制的常见起点。
SameSite=Strict
Set-Cookie: session=…; Secure; HttpOnly; SameSite=Strict
Strict 更严格。跨站进入网站时,相关 Cookie 的发送会受到更强限制。安全边界更清晰,但可能影响从外部链接进入后的登录状态体验,因此上线前要测试真实业务流程。
SameSite=None
Set-Cookie: session=…; Secure; SameSite=None
None 用于确实需要跨站发送 Cookie 的场景。现代浏览器通常要求 SameSite=None 与 Secure 配合。
一旦允许跨站携带会话 Cookie,就更应该使用可靠的 CSRF Token、来源校验等额外措施。
为什么 SameSite 不能完全替代 CSRF Token?
SameSite 能覆盖大量常见攻击路径,但网站可能存在跨站登录、第三方嵌入、旧客户端兼容、复杂子域结构或业务例外。
安全设计不应该把所有保护都押在一个 Cookie 属性上。对于修改密码、修改邮箱、支付、删除数据、权限变更等敏感操作,CSRF Token 仍然是非常重要的验证手段。
CSRF Token 的基本原理
服务器为合法会话生成攻击者无法预测的 Token,并把它放入网站自己生成的页面或应用状态中。提交敏感请求时,客户端必须同时发送这个 Token。
POST /account/email
Cookie: session=USER_SESSION
X-CSRF-Token: RANDOM_UNPREDICTABLE_TOKEN
服务器不仅检查登录会话,还验证 Token 是否属于当前会话或满足既定校验规则。
第三方站点即使能诱导浏览器发起请求,也不应该能够读取目标站点页面并获得正确 Token,这就是 Token 防护的基础。
Token 应该具备什么特征?
- 不可预测,使用安全随机源生成。
- 与会话或用户上下文建立可靠关联。
- 不要把固定 Token 长期硬编码到前端源码。
- 通过 HTTPS 传输。
- 服务端必须真正校验,而不是只在页面里生成一个字段。
- 失败时拒绝敏感操作,并留下适当的安全日志。
Token 是否需要每个请求都变化,要根据实现方式和框架设计决定。关键不是“变化得越频繁越安全”,而是生成、绑定、传输和验证流程都必须可靠。
表单里如何携带 CSRF Token?
传统服务端表单通常会加入隐藏字段:
<form method=”post” action=”/account/email”>
<input type=”hidden” name=”csrf_token” value=”…”>
<input type=”email” name=”email”>
<button type=”submit”>保存</button>
</form>
提交后,服务器验证 csrf_token。Token 不正确、缺失或与当前会话不匹配时,应拒绝请求。
AJAX / Fetch 请求怎么处理?
单页应用或 AJAX 请求经常把 Token 放在自定义请求头中:
fetch(‘/api/profile’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
‘X-CSRF-Token’: csrfToken
},
body: JSON.stringify(data)
})
服务端再读取 X-CSRF-Token 并执行验证。
自定义请求头还能让某些跨站攻击方式更难直接伪造,但仍应结合 CORS、SameSite 和服务端来源验证整体设计。
Origin 校验有什么作用?
浏览器在很多请求中会发送 Origin。服务端可以检查请求是否来自允许的源。
Origin: https://example.com
例如网站只允许来自自身源的敏感写操作,就可以在服务端验证 Origin 是否精确匹配预期协议、主机和端口。
来源校验适合作为额外防线,但不建议用模糊的字符串包含判断。
错误思路:只判断字符串里是否包含 example.com
攻击者可能注册名称相似的域名。应该解析并精确比较允许的 Origin。
Referer 可以作为补充吗?
Referer 在某些场景也能帮助判断请求来源,但它可能受浏览器隐私策略、Referrer-Policy、中间组件等影响。
因此更稳妥的做法是优先使用可靠的 CSRF Token 和 Origin 策略,在明确设计下再把 Referer 作为补充信号,而不是把它当成唯一验证依据。
GET 请求为什么不应该修改重要数据?
CSRF 风险与 HTTP 方法设计密切相关。GET 应用于读取资源,不应该用于删除账号、修改邮箱、开启权限等产生重要副作用的操作。
不推荐:GET /delete-account?id=123
因为链接、图片、预加载、爬虫或用户点击都可能触发 GET。
敏感状态变更应使用适当的 POST、PUT、PATCH、DELETE 等方法,并执行身份、权限和 CSRF 校验。
JSON API 就不会有 CSRF 吗?
不能简单这么判断。
是否存在 CSRF 风险取决于认证方式、Cookie 是否自动携带、CORS、请求 Content-Type、预检行为以及服务端接受哪些请求格式。
如果 API 依赖浏览器自动发送的会话 Cookie,那么仍然应该认真评估 CSRF。不要因为请求体是 JSON 就默认安全。
CORS 和 CSRF 是一回事吗?
不是。
CORS 主要控制浏览器中的跨源读取权限以及某些跨源请求行为;CSRF 关注的是攻击者能否借助受害者的身份触发目标站点上的操作。
即使攻击页面无法读取服务器响应,只要危险操作已经被服务器执行,CSRF 仍然可能成功。
因此不能用“对方读不到响应”来证明不存在 CSRF。
登录接口也需要考虑 CSRF 吗?
需要评估。登录 CSRF 可能让受害者在不知情的情况下进入攻击者控制的账号上下文,之后产生隐私或数据混淆问题。
登录、绑定第三方账号、修改安全设置等身份流程,都应该根据实际认证架构评估来源和 CSRF 防护。
WordPress 是怎么处理 CSRF 类风险的?
WordPress 中经常会看到 nonce。WordPress nonce 用于帮助验证某个操作是否来自预期流程,但它的语义和传统密码学意义上的“一次性随机数”并不完全相同。
开发 WordPress 插件或后台操作时,不应该只检查用户是否登录。通常还应同时验证操作 nonce 和用户权限。
check_admin_referer(…)
check_ajax_referer(…)
current_user_can(…)
具体使用哪个函数应根据后台表单、AJAX、REST 等场景选择,并遵循当前 WordPress API 的设计。
为什么权限校验不能省?
CSRF Token 只能帮助确认请求来自认可流程,它并不能证明当前用户有权执行某个操作。
例如一个普通用户拿到了合法 Token,也不应该因此获得管理员删除用户的权限。
所以服务端通常需要分别完成:
- 身份认证:你是谁?
- 权限校验:你能不能执行这个操作?
- CSRF 校验:这个请求是否来自认可的交互流程?
三者解决的是不同问题。
敏感操作要不要再次确认密码?
对于修改密码、关闭 MFA、变更关键身份信息、资金操作等高风险行为,可以根据风险等级增加重新认证或二次确认。
这不是 CSRF Token 的替代品,而是另一层保护。即使会话被长期保持,也可以避免某些高风险操作仅凭现有会话立即完成。
常见错误:只设置 SameSite 就认为万事大吉
SameSite 很有价值,但业务兼容和浏览器行为会影响实际效果。重要写操作仍应采用服务端 CSRF 防护,而不是只依赖浏览器默认行为。
常见错误:Token 放在 Cookie 里就不校验
如果浏览器会自动发送 Cookie,而服务端又没有要求请求中提供独立可验证的信息,那么可能没有形成有效的请求来源证明。
采用 Double Submit Cookie 等模式时也必须按照完整设计进行比较和绑定,而不是“有一个 csrf Cookie 就算完成”。
常见错误:关闭 CORS 就等于防住 CSRF
CORS 与 CSRF 的安全目标不同。跨站页面即使不能读取响应,也可能尝试提交表单或触发请求。服务端仍需保护状态变更操作。
常见错误:只在前端验证 Token
前端 JavaScript 的检查不能替代服务端验证。攻击者并不需要通过你正常的页面代码发送请求。真正决定是否执行操作的是服务器,因此最终校验必须发生在服务器端。
常见错误:为了方便把所有来源都加入允许列表
Origin、CORS 或可信来源列表都应该尽量精确。通配过宽、信任不受控子域或简单字符串匹配,都可能削弱原本的安全边界。
一套更稳妥的 CSRF 防护组合
对于使用 Cookie 会话的普通 Web 应用,可以把 CSRF 防护理解成多层组合:
- 会话 Cookie 使用 Secure、HttpOnly,并根据业务选择合适的 SameSite。
- 所有敏感状态变更使用正确的 HTTP 方法。
- 敏感写操作验证可靠的 CSRF Token。
- 服务端对 Origin 等请求来源进行合理校验。
- 始终进行独立的身份认证和权限检查。
- 高风险操作根据需要增加重新认证或二次确认。
- 对 CSRF 验证失败和异常来源保留安全日志。
任何单一机制都不应该被当作整个防护体系。
上线前怎么验证?
修改 CSRF 防护后,至少应测试正常流程和失败流程。
- 正常登录用户能否提交表单。
- Token 缺失时是否被拒绝。
- Token 错误时是否被拒绝。
- 无权限用户即使有合法 Token 是否仍被拒绝。
- 跨站 Origin 是否被正确处理。
- 登录、退出、修改资料等流程是否正常。
- 移动端、第三方登录和必要的跨站业务是否受到影响。
安全功能的验收标准不是“页面没有报错”,而是合法请求正常通过,非法请求稳定失败。
总结
CSRF 的本质,是攻击者试图借用浏览器已经拥有的身份凭据,让目标网站执行用户并未真正发起的操作。
SameSite 可以显著减少许多跨站 Cookie 场景,CSRF Token 用来证明请求来自认可流程,Origin/Referer 校验可以提供额外的来源信号,而身份认证与权限控制则决定用户本身是否有资格执行操作。
对于使用 Cookie 会话的网站,更可靠的方案不是在 SameSite、Token 和来源校验之间三选一,而是根据业务把它们组合起来,并对关键状态变更进行服务端验证。