CSRF 利用的是浏览器自动携带身份
当认证依赖 Cookie,浏览器向目标站点发请求时会按 Cookie 规则自动附带会话,发起页面来自哪里并不改变这一基本行为。攻击者不需要读取响应,只要诱导已登录用户提交一个能改变状态的请求,就可能完成改邮箱、创建令牌或删除内容。防护设计因此围绕“服务端如何证明写请求来自受信任的站点交互”,而不是在参数里寻找恶意字符串。XSS 能在同源脚本上下文绕过许多 CSRF 防线,两类风险都要治理。
第一步列出所有会产生副作用的入口,包括传统表单、JSON API、上传、登出、批量操作和看似查询但会触发任务的 GET。GET、HEAD 等安全方法必须保持无副作用,不能因为“按钮方便”把删除做成链接。每个入口标明认证方式、允许来源、请求内容类型和浏览器兼容需求,统一中间件才不会遗漏新路由。仅保护管理页面而不保护它调用的 API 没有意义,攻击者可以直接构造请求。
前端错误处理也不能削弱边界。收到 403 CSRF 错误时只刷新受控 token 或提示重新载入,不把原写请求在后台无限重放;涉及删除、付款和权限的操作需要用户重新确认。Service Worker、离线队列和表单恢复功能若保存写请求,必须同时保存意图版本并在会话变化后失效,不能把旧 token 替换成新值后自动执行数小时前的动作。
Cookie 属性提供基础隔离但不是完整证明
会话 Cookie 使用 Secure 防止明文传输,HttpOnly 减少脚本直接读取,Path 与 Domain 尽量缩小。避免设置宽泛父域 Domain,否则被接管或安全较弱的子域可能进入同站边界。SameSite=Lax 能阻止许多跨站子请求携带 Cookie,并保留常见顶级导航体验;Strict 更强但可能破坏登录回跳,None 则要求 Secure 且显著扩大跨站发送。取值必须结合真实流程在目标浏览器验证。
SameSite 的“site”与同源的 origin 不是同一概念,同站不同子域仍可能处于不同安全水平。历史浏览器、嵌入场景和未来业务调整也可能改变行为,因此它适合作为纵深防御而非唯一授权条件。服务端仍要验证来源或 token。会话建立、刷新和退出同样需要保护,登录 CSRF 可能让用户在攻击者账户下操作并上传敏感数据;退出 CSRF 虽影响较低,也不应通过无保护 GET 完成。
Origin 校验直接约束允许发起写入的站点
浏览器写请求通常携带 Origin,服务端将其解析为严格的 scheme、host、port 三元组,与配置中的精确允许集合比较。不要用字符串 startsWith 或 endsWith,example.com.attacker.invalid 和异常端口会绕过粗糙匹配。反向代理后的目标来源从受信任配置取得,不直接相信任意 Host、X-Forwarded-Host;只有已知代理写入的转发头才能使用。未配置正确生产域名时应拒绝写入,而不是默认放行。
某些旧客户端或隐私环境可能缺少 Origin,可按明确兼容策略检查 Referer 的 origin,但 Referer 可能被省略,不能把“两个头都没有”普遍当作可信。浏览器管理后台通常可以严格拒绝缺失来源;机器 API 应使用不自动附带的 Authorization 凭据并走独立端点,而不是为兼容脚本放宽 Cookie 路由。错误响应只说明来源不受信任,不回显完整内部域名表或会话信息。
CSRF Token 与具体会话绑定并安全比较
同步 token 模式由服务端为会话生成不可预测值,写表单或受控页面取得后通过隐藏字段或自定义头提交,服务端与会话中的值做常量时间比较。token 不能放进会被浏览器自动跨站发送的普通 Cookie 后就直接信任,否则没有增加证明。双提交 Cookie 模式需要对值进行会话绑定或签名,简单比较两个攻击者都可能设置的值会受子域与 Cookie 注入影响。
token 具有足够熵、有限生命周期,并在会话轮换或权限提升后更新。它可以按会话复用以兼顾多标签页,也可以对高风险动作使用一次性挑战;任何策略都要处理后退、重试和并发,而不是让用户频繁遇到假失败。不要把 token 放在 URL,URL 会进入历史、Referer 和日志。前端只向同源目标附带自定义头,通用 HTTP 拦截器不能把 token 发送到第三方 API。
内容类型与 CORS 只能作为附加门槛
JSON API 可以拒绝 application/x-www-form-urlencoded、multipart/form-data 和 text/plain 等浏览器简单请求,并要求自定义 CSRF 头,使跨站 fetch 触发预检。这减少传统表单攻击面,但服务端必须严格解析 Content-Type,不能接受正文像 JSON 就降级处理。上传端点天然需要 multipart,仍使用 Origin 与 token。未来为了兼容客户端放宽内容类型时,安全测试要同步更新。
CORS 决定脚本能否读取或发起被允许的跨源请求,不是 Cookie 写接口的完整 CSRF 授权。Access-Control-Allow-Origin 不能反射任意 Origin 并同时允许凭据,允许列表也要精确匹配。即使响应不可读,简单跨站请求仍可能到达服务端并产生副作用。预检缓存、代理加头和错误路径都要检查,不能只观察前端控制台没有 CORS 错误就认定写操作安全。
高风险操作要求近期认证和最终参数确认
CSRF token 证明请求来自持有页面上下文的一方,却不能证明当前坐在设备前的是账户本人。修改密码、关闭二次验证、导出全部数据、创建长期令牌和批量删除应要求近期重新认证或二次验证,并展示最终目标和影响。确认凭证绑定动作摘要与短有效期,参数改变后失效,避免用户确认一个描述后执行器替换另一组对象。
服务端重新执行资源所有权与角色授权,绝不相信前端隐藏字段中的 userId 或 admin 标志。批量请求限制对象数量并逐项验证,部分失败语义提前定义。审计记录操作者、动作、目标摘要、来源校验方式和结果,不保存密码、Cookie 或 token。权限撤销通过 sessionVersion 等机制立即让旧会话失效,使盗取的页面 token 不能脱离有效会话继续使用。
浏览器测试覆盖跨站、同站子域和正常流程
自动化测试启动两个不同 origin 的站点,在用户已登录目标站的情况下尝试表单 POST、text/plain fetch、图片或导航 GET、缺 token JSON 和伪造 Origin,所有副作用计数必须不变。再测试受控同源页面带正确 token 成功,过期 token、另一会话 token 和参数篡改失败。加入同站不同子域样例,验证 Domain Cookie 和 Origin 策略没有把低信任子域当成后台来源。
代理环境测试 Host 与转发头,确认只有受信任反向代理链影响目标 origin。多标签页、登录轮换、浏览器后退和网络重试用于发现 token 生命周期可用性问题。测试不能只断言状态码,还要查询数据库、审计和外部任务,确保被拒绝请求没有先执行后报错。安全响应不应包含 token、会话内容或内部堆栈,普通日志也要通过敏感字段扫描。
上线从报告与指标发现遗漏入口
统一写请求中间件先记录缺失 Origin、来源不匹配和 token 错误的聚合指标,在不降低防护的前提下识别合法客户端兼容问题。指标按路由和客户端类型分组,避免记录完整 URL 查询或身份内容。拒绝率突增可能是前端未携带 token、代理配置变化或真实攻击,值班手册分别给出核查路径;不能为快速恢复直接关闭全局校验。
新增域名、嵌入应用或跨站登录流程都要重新做威胁评审,精确更新允许来源和 Cookie 属性。定期搜索所有非安全方法路由并与中间件覆盖清单比较,框架升级后重跑浏览器矩阵。CSRF 防线最终由 Cookie 最小范围、SameSite、严格 Origin、会话绑定 token、重新认证和服务端授权共同组成,其中任一层变化都不应悄悄把其他层当作永恒替代。
多标签、API 客户端与缓存边界决定 token 可用性
同一会话的多个标签页可能持有不同时间取得的页面。若每次请求都轮换 CSRF token,后台标签会频繁失败;若永久不变,泄露窗口过长。可按会话固定并在登录、权限提升和会话版本变化时轮换,页面收到 401 后重新认证而非无限刷新 token。高风险一次性挑战只绑定特定动作,不拿它替代所有普通表单 token。
非浏览器 API 客户端使用显式 Authorization 且不依赖自动 Cookie 时,威胁模型不同,可以走独立路由与鉴权。不能为了移动客户端兼容而让同一个 Cookie 端点在缺 Origin、缺 token 时放行。协议选择在服务端从可信认证方式判断,不接受请求参数自称“native client”。API 密钥同样需要最小权限、轮换和防日志泄露。
包含 CSRF token 或用户会话状态的 HTML、SSR payload 和接口响应设置私有缓存策略,不能被 CDN 跨用户复用。公共页面需要表单时可在客户端从同源受保护端点取得 token,端点仍绑定当前会话。缓存问题测试使用两个用户交替请求同一路由,确认 token、Cookie 和错误状态不会穿透,不能只看单用户刷新。
安全中间件的失败应发生在读取巨大正文和执行业务之前,减少攻击资源消耗;但必要的请求头大小也有限制,避免超长 token 成为解析攻击。指标记录校验阶段和路由,不以 token 值作标签。排查时可关联 requestId 与会话匿名摘要,原始 token 从日志、错误跟踪和前端分析中全部排除。
实现片段
assertTrustedOrigin(event); assertCsrfToken(session, headerToken)
评论 · 0