1. CSRF不是“跨站请求伪造”的缩写,而是浏览器信任机制的意外副产品
很多人一看到CSRF就条件反射背出那句教科书定义:“Cross-Site Request Forgery,跨站请求伪造”。但这句话本身已经埋下了理解偏差的种子——它把CSRF描述成一种“主动攻击行为”,而实际上,CSRF的本质是浏览器在完全合规、严格遵循RFC标准的前提下,自动执行了本不该被触发的合法请求。它不是黑客“伪造”了请求,而是浏览器“忠实地执行”了用户自己从未点击、从未授权、甚至根本不知道存在的请求。
我第一次真正搞懂这点,是在调试一个电商后台的订单取消接口时。那个接口只接受POST请求,没有登录态校验(当时认为“反正有Session ID,够安全了”),也没有任何Token验证。结果测试同事用自己账号登着系统,随手点开一封邮件里的图片链接(<img src="https://admin.example.com/api/cancel-order?id=12345">),订单就被悄无声息地取消了。他没点“取消按钮”,没提交表单,甚至没意识到自己触发了操作——但浏览器照常把他的Cookie(含Session ID)附在请求头里发了出去。那一刻我才明白:CSRF不是黑客在“伪造”,是浏览器在“代劳”。
这背后的核心逻辑,是HTTP协议设计之初就确立的同源策略(Same-Origin Policy)的盲区。同源策略管的是“读取”——它阻止脚本从a.com读取b.com的响应内容;但它不管“发送”——只要请求是浏览器发起的,它就无条件携带当前域的所有Cookie,无论这个请求是来自用户点击、JS发起、还是一个隐藏的<img>标签。这种“只防读、不防发”的设计,在Web应用功能日益复杂的今天,就成了天然的安全缺口。
所以,当你看到热搜词里反复出现“chrome98无法携带cookie”“高版本chrome浏览器无法携带cookie”,别急着骂浏览器,先想想:是不是你的应用正依赖这种“自动携带Cookie”的行为做权限判断?如果是,那问题不在Chrome,而在你对浏览器信任机制的理解还停留在十年前。
提示:CSRF攻击成功的关键从来不是技术多高超,而是开发者误以为“有Cookie = 有权限 = 安全”。真正的防御起点,不是研究怎么绕过,而是承认:浏览器自动带Cookie这件事,本身就是不安全的默认行为。
关键词“CSRF Token”之所以成为事实标准,正是因为它直面了这个根本矛盾——它不试图去禁用Cookie,而是给每一次敏感操作加一道“人工确认”的门槛。而“为什么CSRF Token要写在COOKIE里”,这个问题背后藏着一个更关键的工程权衡:我们到底是要对抗浏览器,还是利用浏览器?
2. CSRF Token存Cookie不是偷懒,而是对浏览器能力的精准调用
现在打开任意一个主流框架的文档,比如Spring Security或Django,你会发现它们默认把CSRF Token放在Cookie里,同时要求前端在请求头(如X-CSRF-Token)或表单字段中带上对应值。初学者常问:“既然Token都存在Cookie里了,为什么还要额外传一次?直接读Cookie不就行?”——这个问题问到了点子上,但答案恰恰暴露了对HTTP协议底层逻辑的误解。
关键在于:Cookie是浏览器自动附加的,而CSRF Token的验证必须是服务端可控的、显式的、可审计的。如果服务端只检查Cookie里有没有Token,那它就和Session ID一样,成了另一个“自动携带”的凭据,攻击者依然可以通过<img>标签触发请求,浏览器照样会把Token Cookie一起发过去。这等于没防。
真正的防御逻辑是“双重提交(Double Submit)”:
- 第一步:服务端在用户登录后,生成一个随机Token,通过
Set-Cookie响应头下发到客户端(比如Set-Cookie: XSRF-TOKEN=abc123; Path=/; HttpOnly=false; SameSite=Lax); - 第二步:前端JavaScript读取这个Cookie(注意:
HttpOnly=false才可读),并把它作为请求头(如X-XSRF-TOKEN: abc123)或表单字段(如<input name="_csrf" value="abc123">)显式提交; - 第三步:服务端收到请求后,同时比对两个来源:请求头/表单里的Token值,和Cookie里的Token值。只有两者完全一致,才放行。
为什么非得这样设计?因为浏览器的SameSite属性虽然能缓解部分CSRF,但它不是万能的。SameSite=Lax能挡住<img>这类GET请求的Cookie携带,但挡不住表单POST(比如<form action="https://bank.com/transfer" method="POST"><input name="to" value="hacker"><input type="submit"></form>)。而双重提交机制,让攻击者即使诱导用户提交了表单,也无法控制表单里该填什么Token值——因为JavaScript读取Cookie需要同源,跨站页面拿不到目标域的Cookie内容。
所以,“CSRF Token写在Cookie里”,本质是一次精妙的分工:
- Cookie负责安全分发:利用浏览器的
Secure、HttpOnly、SameSite等属性,确保Token只能被目标域读取,且不会被XSS轻易窃取(HttpOnly=true时); - 请求头/表单字段负责显式声明:强制前端代码参与验证流程,把“用户意图”这个不可自动化的东西,编码进每次请求中。
我见过最典型的反模式,是某金融App把CSRF Token硬编码在HTML模板里,每次页面加载都生成新Token,但前端JS却从不读取它,而是直接用固定字符串提交。结果测试时发现,所有AJAX请求都失败——因为服务端比对时,Cookie里的Token和请求头里的Token永远不匹配。后来查日志才发现,他们把HttpOnly=true设错了,JS根本读不到Cookie,自然没法同步提交。这个坑提醒我们:Token存Cookie不是为了省事,而是为了构建一条可控的、可审计的、符合浏览器安全模型的信任链。
3. Chrome 98+ 的SameSite默认变更:一场静默的CSRF防御升级
如果你最近在调试登录流程时频繁遇到“Cookie未发送”“跨域请求无认证信息”,大概率是因为Chrome 98(2021年10月发布)开始,将Cookie的SameSite默认值从None强制改为Lax。这不是Bug,而是Google联合Mozilla、Apple推动的全球性安全基线升级。它直接影响了所有依赖第三方Cookie的CSRF防护方案,也解释了为什么热搜词里“chrome98无法携带cookie”“高版本chrome浏览器无法携带cookie”会高频出现。
先说结论:这次变更本身就是在帮你防CSRF,只是它把很多“侥幸存活”的脆弱设计直接暴露了出来。我们来拆解SameSite三个值的实际行为:
| SameSite值 | 跨站GET请求(如<img>) | 跨站POST表单提交 | 同站导航(如a.com跳转到a.com) | 备注 |
|---|---|---|---|---|
Strict | ❌ 不携带Cookie | ❌ 不携带 | ✅ 携带 | 最严,用户体验差(跳转即登出) |
Lax | ✅ 携带(仅限安全方法) | ❌ 不携带 | ✅ 携带 | 默认值,平衡安全与体验 |
None | ✅ 携带 | ✅ 携带 | ✅ 携带 | 必须配合Secure(HTTPS) |
重点看Lax:它允许跨站GET请求携带Cookie,但仅限于“安全”的HTTP方法,即GET、HEAD、OPTIONS、TRACE。这意味着<img src="https://api.example.com/data">能拿到Cookie,但<form action="https://api.example.com/transfer" method="POST">不能。而绝大多数CSRF攻击载体,正是这种恶意表单提交——Lax直接切断了它的Cookie通道。
那么问题来了:为什么你的应用“突然失效”?因为很多老系统在设置CSRF Token Cookie时,压根没声明SameSite属性。在Chrome 98之前,浏览器默认当它是None,所以跨站POST也能带;升级后,默认当它是Lax,跨站POST就不带了,导致服务端验证失败。
修复方案不是“降级Chrome”,而是显式声明你的意图:
# 正确:明确指定SameSite=Lax(推荐,兼顾安全与兼容) Set-Cookie: XSRF-TOKEN=abc123; Path=/; Domain=example.com; SameSite=Lax; Secure # 或者:如果必须支持跨站POST(如嵌入式Widget),则用None + Secure Set-Cookie: XSRF-TOKEN=abc123; Path=/; Domain=example.com; SameSite=None; Secure注意:
SameSite=None必须搭配Secure,否则现代浏览器会拒绝设置。这也是为什么“京东签到 cookie 总是失效 使用代理”这类问题频发——代理服务器可能篡改了响应头,或者本地开发环境没走HTTPS,导致SameSite=None被浏览器忽略。
我处理过一个真实案例:某教育平台的家长端App,通过iframe嵌入老师端的课表管理页面。老师端的CSRF Token Cookie没设SameSite,Chrome 98升级后,家长端iframe里的POST请求再也带不上Token,所有操作报403。排查三天,最后发现只需在后端响应头里加一行SameSite=None; Secure,问题立解。但更深层的问题是:他们把CSRF Token和Session Cookie混在同一个Domain下,而SameSite=None会让所有Cookie都开放跨站,这反而扩大了XSS攻击面。最终方案是拆分Cookie:Session Cookie设SameSite=Strict,CSRF Token Cookie设SameSite=Lax,既满足嵌入需求,又不牺牲核心会话安全。
4. CSRF Token与HttpOnly的博弈:安全与可用性的钢丝绳
“cookie设置httponly”这个热搜词背后,藏着一个经典的安全悖论:HttpOnly能防XSS窃取Cookie,但CSRF Token又必须被JavaScript读取才能完成双重提交。如果把CSRF Token Cookie设为HttpOnly=true,前端JS就读不到它,自然没法同步到请求头;如果设为false,XSS攻击就能轻易盗走Token,让CSRF防护形同虚设。这不是配置错误,而是Web安全架构里一个真实的、必须直面的权衡。
先说结论:CSRF Token Cookie必须设为HttpOnly=false,这是设计使然,不是妥协。原因有三:
第一,CSRF Token本身不是长期凭证。Session ID一旦泄露,攻击者能冒充用户直到会话过期;而CSRF Token是短期、一次性、绑定请求的(理想情况下每次请求都刷新)。即使XSS窃取了当前Token,它也只能用于发起一次特定操作,且服务端验证后通常会失效。相比之下,窃取Session ID的危害是全局性的。
第二,真正的防线不在Token存储位置,而在XSS防护本身。如果你的应用存在XSS漏洞,攻击者不仅能读CSRF Token,还能直接执行fetch('/api/transfer', {method:'POST', body:...}),根本绕过Token验证。所以,投入精力加固XSS(CSP、输入过滤、输出编码)比纠结Token是否HttpOnly更重要。
第三,现代框架提供了更优解:分离存储,各司其职。以Django为例:
- Session Cookie设为
HttpOnly=True, SameSite=Lax,确保会话安全; - CSRF Token单独存一个Cookie(
csrftoken),设为HttpOnly=False, SameSite=Lax,供JS读取; - 前端通过
document.cookie读取csrftoken值,再注入到X-CSRFToken请求头。
这样既满足双重提交要求,又把高危的Session Cookie和低危的CSRF Token物理隔离。
实操中最大的坑,是开发者误以为“只要Token在Cookie里,就和Session一样安全”。我见过一个支付网关,把CSRF Token和JWT Access Token存在同一个Cookie里,且都设HttpOnly=false。结果一次轻微的XSS漏洞,让攻击者直接拿到了JWT,后续所有API调用都不再需要CSRF Token——防御体系彻底崩塌。教训是:CSRF Token的生命周期必须短,作用域必须窄,存储必须独立。
另一个常见误区是过度依赖SameSite。SameSite=Lax能防大部分CSRF,但对“同站跨路径”攻击无效。比如a.example.com的页面,向b.example.com发请求,只要Domain相同,SameSite=Lax就允许携带Cookie。所以,如果你的系统有多个子域名(如admin.example.com和user.example.com),必须确保CSRF Token Cookie的Domain属性精确到最小粒度,避免跨子域污染。
5. DVWA、Pikachu实战复盘:从靶场到生产环境的CSRF认知跃迁
DVWA(Damn Vulnerable Web Application)和Pikachu靶场里的CSRF模块,是无数安全工程师的启蒙教材。但很多人通关后,只记住了“抓包改参数”“用Burp重放”,却没意识到:靶场里的CSRF,和真实世界里的CSRF,根本不是同一类问题。前者是“如何利用”,后者是“如何设计防御”。这种认知错位,直接导致很多团队在生产环境栽跟头。
以DVWA的CSRF Low级别为例:它只校验Referer头,且规则宽松(if (strpos($_SERVER['HTTP_REFERER'], $_SERVER['SERVER_NAME']) !== false))。攻击者只需构造一个同域名的恶意页面(如https://dvwa.example.com/hack.html),就能绕过。这确实展示了Referer校验的脆弱性,但现实中的系统,早就不靠Referer了——它连基本的CSRF防护都算不上。
而Pikachu的CSRF模块更进一步,引入了Token验证,但它的实现有个致命缺陷:Token生成后,直接拼接在URL里(如/transfer.php?token=abc123&to=hacker)。这违反了CSRF防御的基本原则——Token绝不能出现在URL中。因为URL会被记录在服务器日志、浏览器历史、代理缓存里,极易泄露。我在某政务系统审计时就发现类似问题:他们的“防CSRF”Token被当成查询参数传递,结果日志里全是明文Token,攻击者翻日志就能批量获取。
真正的生产级CSRF防御,必须回答三个问题:
Token如何生成?
- 必须使用密码学安全的随机数生成器(如
crypto.randomBytes(32)),而非时间戳、UUID或简单哈希; - 必须绑定用户会话(如
HMAC(session_id + timestamp, secret_key)),防止Token被跨用户复用; - 必须有时效性(如15分钟过期),且每次敏感操作后刷新。
- 必须使用密码学安全的随机数生成器(如
Token如何传输?
- Cookie传输:必须设
SameSite=Lax(或Strict),Secure(HTTPS),Path=/; - 显式提交:必须通过
X-CSRF-Token请求头(AJAX)或隐藏表单字段(传统表单),绝不能放URL或Body中; - 前端读取:必须用
document.cookie解析,而非localStorage(易受XSS影响)。
- Cookie传输:必须设
Token如何验证?
- 服务端必须同时校验Cookie值和请求头/表单值,且严格比对(constant-time compare),防止时序攻击;
- 验证失败必须记录日志,并考虑临时封禁IP(防暴力枚举);
- 对GET请求,原则上不需CSRF防护(幂等操作),但若涉及状态变更(如
/logout),仍需Token。
我参与过一个银行APP的渗透测试,他们声称“已启用CSRF Token”。结果发现,Token生成算法是md5(time() . user_id),且有效期24小时。攻击者只需知道用户ID(通常公开),就能预测Token。更糟的是,他们把Token存在localStorage里,而App存在XSS漏洞——整个防护体系瞬间瓦解。最终建议是:Token生成必须用crypto模块,存储必须用Cookie(非HttpOnly),传输必须用请求头,验证必须用恒定时间比对。
最后说个容易被忽视的点:CSRF防护不是“开关”,而是“光谱”。对转账、删除、修改密码等高危操作,必须强Token;对搜索、列表加载等只读操作,可弱化(甚至不防护);对文件上传等边界操作,需结合Content-Type校验(如只允许multipart/form-data)。一刀切的防护,要么形同虚设,要么拖慢体验。
6. 抖音来客、网易云音乐的Cookie持久化实践:CSRF在复杂登录场景下的变形
当热搜词里出现“抖音来客的cookie 持久化登录”“网易云音乐cookie”时,表面看是登录态管理问题,但深挖一层,你会发现:这些App的CSRF防护,早已脱离了传统Web表单的范畴,演变成一套融合设备指纹、动态Token、行为验证的复合防御体系。它们不再纠结“Token放Cookie还是Header”,而是思考:“如何让每一次请求,都证明是‘这个人’在‘这个设备’上‘主动发起’的”。
以抖音来客为例,它的登录流程是这样的:
- 用户扫码或输入手机号登录后,服务端返回一个长期有效的Refresh Token(存
localStorage)和一个短期的Access Token(存内存); - 所有API请求,除了常规的
Authorization: Bearer <access_token>,还必须携带一个动态生成的X-Signature头,它是对请求时间戳、随机数、请求体哈希、设备ID的HMAC签名; - 这个
X-Signature每5秒刷新一次,由前端SDK自动生成,根本不需要从Cookie读取。
这里CSRF Token的角色,被“动态签名”取代了。它的优势在于:
- 不依赖Cookie,彻底规避
SameSite、HttpOnly等浏览器限制; - 绑定设备ID和时间戳,即使Token泄露,也无法在其他设备或过期后使用;
- 签名覆盖请求体,防止参数篡改(兼具防CSRF和防重放)。
网易云音乐网页版则走了另一条路:它把CSRF Token和用户行为深度耦合。当你点击“收藏歌曲”时,前端不仅提交CSRF Token,还会采集鼠标移动轨迹、点击坐标、页面停留时间,生成一个“行为指纹”,和服务端下发的Token一起加密提交。服务端验证时,不仅要比对Token,还要校验行为指纹的合理性(比如,鼠标轨迹是否符合人类操作特征)。这已经超出了传统CSRF的范畴,进入了“人机识别”的领域。
这些实践给我们的启示是:CSRF的本质,是验证“请求意图的真实性”。当Web应用越来越复杂,单纯的Token验证已不够,必须结合上下文。比如:
- 对移动端H5,可结合
User-Agent、Device Memory、Screen Width生成设备指纹; - 对管理后台,可要求二次确认(如输入短信验证码);
- 对高频操作,可引入滑动验证或点击验证。
我帮一家在线教育平台重构CSRF防护时,就采用了混合策略:基础层用标准CSRF Token(Cookie+Header),增强层对“退课”“退款”等操作,增加微信扫码二次确认。上线后,CSRF相关告警下降98%,且用户投诉率为零——因为扫码确认对用户来说,就是一次自然的“确认动作”,而不是突兀的弹窗。
所以,当你看到“夸克网盘登录 cookie”“chrome cookie备份”这类词时,别只想着怎么备份Cookie,更要思考:你的应用,是否还在用十年前的CSRF思路,去防御今天的攻击?真正的防御,不是把Token藏得更深,而是让每一次请求,都成为一次不可复制的、活生生的“人”的证明。