想象你是一家银行的老客户。你穿着体面的西装走进银行大楼,门卫看到你的会员卡就放行了——这张卡就是你的“自动通行证”。隔壁的骗子开了个“皮包公司”,专门请你参观。他的网页里藏着一道机关:一旦打开,它就会指挥你的“私人助理”(你手中的浏览器)自动跑回银行,去办理一笔你根本没想办的转账。银行柜员看到通行证自动刷过,以为是你本人要办业务,稀里糊涂就把钱转了出去——这就是跨站请求伪造:骗子不偷你的卡,而是让你的卡“替他自动办事”。 怎么防?银行想了个土办法:办关键业务时,柜员给你一张“取款令牌”,你必须把令牌填进业务单里一起提交。令牌是随机生成的,骗子根本拿不到,伪造的业务单被柜员一眼识破。这还不够,银行又给通行证印上“仅限本大楼使用”的标签,让保安登记你是从哪扇门进来的。多道防线层层设卡,骗子就再也钻不了空子。 这套“银行安保系统”对应着Web安全里著名的CSRF Token机制。这篇教程将用这个比喻,带你从原理到代码完整掌握它——零基础也能看懂。

岗位关联

对AI从业者而言,CSRF Token机制是Web应用安全的基本功。无论是开发对外API、搭建模型服务平台,还是审计大模型应用的Web框架,掌握这套防御思路都能帮你堵住真实漏洞,提升系统的可靠性与可信度。

目录
  1. 可视化演示
  2. CSRF攻击原理
  3. 攻击类型分类
  4. 同步令牌模式
  5. 令牌生成安全
  6. 双重提交方案
  7. SameSite防护
  8. 来源校验策略
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

CSRF Token通过在请求中嵌入不可预测令牌防御跨站请求伪造。本文详解CSRF攻击原理与利用条件、同步To... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

CSRF攻击:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

CSRF攻击原理

想象一座老式写字楼,门禁系统只认门禁卡不认人。你持卡进入后,你的“助理”(浏览器)会一直帮你自动刷卡。骗子从隔壁公司发现:只要诱导你去访问他的页面,页面里的机关就能让助理“替我去银行办业务”——助理到了门口自动刷卡,门卫照样放行。这里的漏洞在于:门禁认为“刷卡的人”就等于“持卡人”,但刷卡的动作其实是助理自动完成的。

在Web世界里,“助理自动刷卡”就是浏览器自动携带Cookie。当你登录过目标站点,浏览器会记住该站点的Cookie,之后无论你是主动点击链接,还是页面里自动加载了图片、脚本,浏览器向该站点发请求时都会自动带上Cookie。攻击者只需要构造一个指向目标站点的请求并诱导浏览器发出,服务器就会把它当成“你本人”的操作。

CSRF的全称是Cross-Site Request Forgery(跨站请求伪造)。它不是窃取你的数据,而是“借用你的身份”去发号施令。一次成功的CSRF攻击需要三个条件同时满足:第一,你已在该站点登录且会话Cookie有效;第二,目标请求正好使用了浏览器自动携带凭证的机制;第三,服务器没有额外校验“这个请求是不是用户有意图发起的”。用逻辑公式可以表达为:

可见,CSRF的根源是“身份验证”与“意图验证”的混淆——门禁认证了卡,却没有认证人的意愿。要防御,就必须为请求加上第二把钥匙,这把钥匙就是后面要讲的Token。

Scsrf=A∧C∧¬TS_{\text{csrf}} = A \land C \land \lnot T
这个公式的意思是:攻击成功,当且仅当「用户已认证」且「凭证自动携带」且「没有Token校验」同时成立。任何一个条件不满足,攻击都会失败。
  1. 攻击成功变量用 \(S_{\text{csrf}}\) 表示一次跨站请求能否攻击成功,取 1 代表成功,0 代表失败。
    Scsrf=A∧C∧¬TS_{\text{csrf}} = A \land C \land \lnot T
  2. 已认证条件\(A\) 取 1 表示用户已登录目标站点,会话 Cookie 仍然有效,相当于“门禁卡在有效期内”。
    A=1A = 1
  3. 自动携带条件\(C\) 取 1 表示浏览器会随请求自动附带 Cookie 凭证,相当于“助理会自动刷卡”。
    C=1C = 1
  4. 逻辑与\(\land\) 读作“且”,三个条件必须同时为 1 攻击才成立,缺一个都不行。
    1∧1∧1=11 \land 1 \land 1 = 1

攻击类型分类

同样是让助理去银行递业务单,骗子递单子的方式五花八门。有的把业务单夹在图片里——“帮我看一眼这张图”,助理一看图就自动跑腿了;有的伪造一张表格混在文件堆里,助理一翻页就自动填好提交了。骗子的手法人不同,但本质都是“让助理以为这是主人吩咐的”。

在Web请求里,“递单子的方式”就是请求方法。GET请求把参数放在URL里,原本用于查询;POST请求把参数放在请求体里,用于提交数据。攻击者会根据目标接口的设计,选择不同的载体。最常见的三种CSRF攻击形式,分别对应GET、POST和JSON接口。

GET型攻击:攻击者在恶意页面里写一个<img src="https://bank.com/api/transfer?to=hacker&amount=1000">标签。当浏览器加载这个页面时,会自动请求这个URL,从而触发一次GET请求。POST型攻击:攻击者构造一个隐藏表单,配合一小段JavaScript在页面加载时自动调用form.submit(),向目标接口提交POST请求。JSON型攻击:针对接收application/json格式的接口,攻击者可以用fetch方法自动发送一个带JSON体的跨站请求。

无论哪种载体,路径都是同一条:攻击者的页面指挥浏览器、浏览器自动带卡、服务器见卡就放行。所以防御思路不是去堵某一种载体,而是让服务器学会识别“这张单子是不是主人亲手递的”。

攻击载体={img 标签GET型攻击隐藏表单POST型攻击fetch 脚本JSON型攻击\text{攻击载体} = \begin{cases} \text{img 标签} & \text{GET型攻击} \\ \text{隐藏表单} & \text{POST型攻击} \\ \text{fetch 脚本} & \text{JSON型攻击} \end{cases}
这个分段函数表示:攻击者根据请求方法选择对应的攻击载体,三种载体的共同点是都能让浏览器“自动”发出请求。
  1. 攻击载体攻击者用来触发跨站请求的具体手段,也就是“骗子递单子的方式”。
    攻击载体\text{攻击载体}
  2. img 标签对应 GET 型攻击,浏览器解析图片 URL 时自动发出 GET 请求,不需要任何用户点击。
    img 标签→GET 型攻击\text{img 标签} \rightarrow \text{GET 型攻击}
  3. 隐藏表单对应 POST 型攻击,页面加载时 JavaScript 自动提交表单,浏览器按表单指定方法发出 POST。
    隐藏表单→POST 型攻击\text{隐藏表单} \rightarrow \text{POST 型攻击}
  4. fetch 脚本对应 JSON 型攻击,fetch API 可以在不刷新页面的情况下发送任意方法和内容类型。
    fetch 脚本→JSON 型攻击\text{fetch 脚本} \rightarrow \text{JSON 型攻击}

同步令牌模式

银行终于改了制度:办大额业务时,柜员会先递给你一张盖了钢印的取款牌,取款时要把取款牌连同业务单一起交回。柜员对照自己留存的登记簿,号码一致才办理。骗子就算能指挥助理跑腿,也拿不出那张取款牌——因为取款牌只在柜员和你的手里。

这就是同步Token模式(Synchronizer Token Pattern,也叫同步令牌模式),OWASP推荐的经典CSRF防御方案。服务器在渲染页面时,为用户会话生成一个不可预测的Token,存进Session,同时把它写入表单的隐藏字段。用户提交表单时,服务器取出隐藏字段里的Token,与Session中保存的Token比对,一致才处理这个请求。

整个流程可以概括为四步:用户请求页面,服务器生成Token并存入Session,同时下发到页面表单;用户填写并提交表单,Token随表单字段一起返回;服务器从请求中取出Token,与会话中的Token做对比;一致则放行,不一致则拒绝(通常返回403)。这个模式的关键在于:Token必须绑定会话、必须不可预测、必须由服务端校验。攻击者无法从目标站点页面之外的地方拿到这个Token,自然无法在伪造请求中提交合法令牌。

柜员对存根,就是这个模式最朴素也最可靠的形态。同步Token把“身份凭证”和“意愿凭证”分开了:Cookie证明“你是谁”,Token证明“你确实想办这件事”。

T提交=TSession⇒放行T_{\text{提交}} = T_{\text{Session}} \Rightarrow \text{放行}
公式的含义是:请求中带回来的 Token,必须与服务器 Session 里保存的 Token 完全相等,服务器才放行这个请求。
  1. 提交令牌\(T_{\text{提交}}\) 是表单隐藏字段(如 _csrf)中带回服务器的 Token 值,相当于业务单上的取款牌编号。
    T提交T_{\text{提交}}
  2. 会话令牌\(T_{\text{Session}}\) 是服务器在用户会话中保存的 Token 值,相当于柜员的登记簿存根。
    TSessionT_{\text{Session}}
  3. 相等比较两边的 Token 必须逐字节完全相同,部分匹配不算数。
    T提交=TSessionT_{\text{提交}} = T_{\text{Session}}
  4. 放行结论\(\Rightarrow\) 读作“蕴含”或“则”,只有比较通过,服务器才会继续处理请求,否则一律拒绝。
    T提交=TSession⇒放行T_{\text{提交}} = T_{\text{Session}} \Rightarrow \text{放行}

令牌生成安全

取款牌如果只是一张白纸,骗子复印几张就完事了。银行于是把取款牌升级为带随机防伪纹理的工艺卡——每一张的纹理都由加密级随机机生成,连柜员都预测不了下一张长什么样。Token的安全完全建立在“不可预测”之上。

如果Token的生成使用了可预测的算法——比如只用当前时间戳做MD5——攻击者就可以自己算出一堆候选Token来暴力尝试。因此,Token必须使用密码学安全的随机数来源:在Python中使用secrets模块,在Node.js中使用crypto.randomBytes,这些都是操作系统提供的安全随机源。普通的Math.random()、rand()等伪随机数生成器,不适合用来生成Token。

Token的强度由熵决定,熵可以理解成“随机比特数”。若Token的熵是 \(n\) 比特,攻击者猜中特定Token的概率就是:

实际工程中,还会用HMAC-SHA256对“会话信息+随机数”做签名来生成Token,这样即使两个用户拿到的是同一批随机种子,最终Token也不可互相推导。防伪纹理决定了取款牌能不能被仿造,随机熵则决定了Token能不能被猜中——这些“工艺”做到位,伪造在计算上才真正不可能。

P猜中=2−nP_{\text{猜中}} = 2^{-n}
这个公式说明:Token的随机比特数越多,攻击者猜中的概率就越低,并且是“指数级”下降。
  1. 猜测概率\(P_{\text{猜中}}\) 表示攻击者在一次尝试中猜中合法 Token 的概率。
    P猜中P_{\text{猜中}}
  2. 指数分母\(2^{-n}\) 等价于 \(1/2^n\),其中 \(n\) 是 Token 的有效熵比特数。
    2−n=12n2^{-n} = \frac{1}{2^n}
  3. 熵的威力当 \(n=256\) 时,猜测概率约等于 \(1/(1.15 \times 10^{77})\),在计算上完全不可行。
    2−256≈10−772^{-256} \approx 10^{-77}
  4. 安全下限OWASP 建议 Token 至少要有 128 比特熵;实际工程中常用 32 字节(256 比特)安全随机数。
    n≥128n \geq 128

双重提交方案

有的银行网点是临时柜台,没有存根登记簿(对应无状态服务)。柜员想了个办法:让客户在业务单上签一次名,再在门禁卡背面也签一次名,柜员比对两个签名是不是同一笔迹。骗子虽然能指使助理送单,却看不到门禁卡背面的签名,所以永远签不出一样的字。

双重提交Cookie方案(Double-Submit Cookie)正是这个思路:服务端生成Token后,通过Set-Cookie把Token下发给浏览器,同时让页面里的表单或脚本在请求参数(或自定义请求头)里再带一份相同的Token。请求到达服务端时,服务端只需要比对Cookie里的值与请求参数里的值是否一致,无需查询Session。

为什么攻击者伪造不了?因为浏览器的同源策略禁止跨域脚本读取目标站点的Cookie,攻击者无法得知Cookie令牌的值,也就无法在表单参数里填入相同的Token。这个方案的最大优势是服务端不需要存储会话状态,非常适合无状态、水平扩展的架构。但要注意前提:如果站点的子域名存在XSS等漏洞,攻击者可能读取Cookie进而同时伪造两个值,因此实际部署时常配合加签Cookie、__Host-前缀等手段。

双签名的妙处在于:攻击者控制得了“单子怎么递”,却控制不了“门禁卡背面长什么样”。服务器不靠记忆,只靠两个值的交叉验证,就能识破伪造。

TCookie=T参数⇒放行T_{\text{Cookie}} = T_{\text{参数}} \Rightarrow \text{放行}
公式的含义是:请求中参数携带的 Token,必须与浏览器 Cookie 中携带的 Token 完全一致,服务器才放行。
  1. Cookie令牌\(T_{\text{Cookie}}\) 是浏览器通过 Set-Cookie 存下、并在每个请求里自动携带的令牌值,攻击者跨域读不到。
    TCookieT_{\text{Cookie}}
  2. 参数令牌\(T_{\text{参数}}\) 是表单隐藏字段或自定义请求头(如 X-CSRF-Token)中提交的令牌值。
    T参数T_{\text{参数}}
  3. 比对逻辑服务端对两个值做恒定时间比较,内容一致才算通过。
    TCookie=T参数T_{\text{Cookie}} = T_{\text{参数}}
  4. 判定结果一致才放行,否则拒绝该请求。
    TCookie=T参数⇒放行T_{\text{Cookie}} = T_{\text{参数}} \Rightarrow \text{放行}

SameSite防护

银行给门禁卡加印了一行字:“仅限本大楼使用”。你拿着这张卡去隔壁商场,商场门禁会自动拒收,卡根本刷不出去。浏览器里的SameSite属性,就是印在Cookie上的这行字——它由浏览器执行,跨站请求时自动“拒收”Cookie。

SameSite是Cookie的一个属性,用来告诉浏览器:跨站请求中不要携带本Cookie。它有三个取值:Strict表示完全禁止跨站携带;Lax表示只允许顶层导航(用户点击链接跳转)时携带,跨站的子资源请求(如图片、iframe、fetch)都不带;None表示不限制,但必须配合Secure和HTTPS才能使用。现代浏览器已将Lax设为默认值。

要正确理解SameSite,需要区分“站”和“源”:站指注册域名(eTLD+1),比如a.example.com和b.example.com属于同站,但example.com和attacker.com是跨站。Lax作为默认值已经能挡住绝大多数传统CSRF攻击——因为攻击者常用的img、表单自动提交都属于跨站子资源请求,不会带Cookie。但Lax不等于万能:顶层导航的GET请求仍会带Cookie,如果某些接口用GET就能改数据(这是设计反模式),仍有风险;且老旧浏览器不支持SameSite属性。因此它通常是防御的第一道门,服务端Token仍然是最终裁决。

“仅限本大楼使用”的标签把防御前移到了浏览器层,攻击者连“进门刷卡”的机会都没有。但标签不能替代柜员核对取款牌——两道防线互相配合,才是纵深防御。

C携带={1同站请求1Lax 且顶层导航0Strict 且跨站C_{\text{携带}} = \begin{cases} 1 & \text{同站请求} \\ 1 & \text{Lax 且顶层导航} \\ 0 & \text{Strict 且跨站} \end{cases}
这个分段公式描述了Cookie是否被携带的三条规则:同站永远带;Lax模式只放行顶层导航;Strict模式下跨站一律不带。
  1. 携带结果\(C_{\text{携带}}\) 取 1 表示浏览器会随请求携带 Cookie,取 0 表示不带。
    C携带C_{\text{携带}}
  2. 同站直通请求与 Cookie 归属于同一站时,任何 SameSite 取值都会携带 Cookie。
    C携带=1(同站)C_{\text{携带}} = 1 \quad (\text{同站})
  3. Lax 放行Lax 模式下,只有顶层导航请求(用户点击链接跳转)才允许携带。
    C携带=1(Lax∧顶层导航)C_{\text{携带}} = 1 \quad (\text{Lax} \land \text{顶层导航})
  4. Strict 拒绝Strict 模式下,任何跨站请求都不携带,相当于“卡在别的楼直接被吞”。
    C携带=0(Strict∧跨站)C_{\text{携带}} = 0 \quad (\text{Strict} \land \text{跨站})

来源校验策略

银行大厅门口新增了保安岗,每位进来的客户都要登记来路:是从银行正门进,还是从隔壁商场穿越过来的。来路可疑的,直接拦下。这个“来路登记”,对应HTTP请求里的Origin与Referer头。

Origin头记录了请求来源的协议、域名和端口,例如https://evil.com。它是浏览器在跨站请求时自动附加的,非常可靠。Referer头则记录了来源页面的完整URL,但可能因为隐私策略(如Referrer-Policy: no-referrer)被省略,也可能被代理剥离。所以服务端校验时,通常优先使用Origin,缺失时再用Referer兜底。

具体的防御策略是:在服务端配置一个白名单,只包含本站和可信合作方的完整来源组合(协议+域名+端口)。对每个请求,先查看Origin是否在白名单中,在则放行,不在则拒绝;如果Origin为空,则查看Referer并做同样的白名单匹配;两者都为空时,更严格的服务端应该拒绝请求,或要求以Token做最终验证。白名单必须精确到端口,并且不能简单用字符串包含判断——比如evil-bank.com这种域名伪装,要严格做完整匹配,避免被“域名前缀”绕过。

来客登记是比取款牌更轻量的第一道筛子:它不看身份真假,只看“从哪来”。因为Referer可能“没登记”,保安最终还会请柜员用Token再验一道——两者结合,才让攻击者的伪装处处漏风。

O请求∈W白名单⇒放行,O请求=scheme://host:portO_{\text{请求}} \in W_{\text{白名单}} \Rightarrow \text{放行}, \quad O_{\text{请求}} = \text{scheme}://\text{host}:\text{port}
公式的含义是:请求的Origin必须属于服务端白名单才放行;而Origin本身是一个包含协议、域名、端口的三元组,任何一部分不匹配都算不在白名单中。
  1. Origin三元组\(O_{\text{请求}}\) 表示请求头 Origin 的值,由协议、主机名、端口三部分组成。
    O请求=scheme://host:portO_{\text{请求}} = \text{scheme}://\text{host}:\text{port}
  2. 白名单集合\(W_{\text{白名单}}\) 是服务端预先配置的可信来源集合,只收录本站和可信合作方。
    W白名单={可信任来源}W_{\text{白名单}} = \{ \text{可信任来源} \}
  3. 成员判断\(\in\) 表示“属于”,即 Origin 必须能在这个集合中找到完全匹配的项。
    O请求∈W白名单O_{\text{请求}} \in W_{\text{白名单}}
  4. 放行规则命中白名单才继续处理请求,否则直接拒绝;Origin 缺失时改用 Referer 兜底,再缺失则交给 Token 验证。
    O请求∈W白名单⇒放行O_{\text{请求}} \in W_{\text{白名单}} \Rightarrow \text{放行}

AI 应用场景

AI模型服务API的CSRF防护

阶段:模型服务部署与安全加固

解决的问题:AI平台对外提供Web界面管理模型仓库、训练任务和推理资源。这些管理接口如果只靠Cookie会话认证,攻击者可以诱导已登录的算法工程师在不知情时提交请求,删除模型、修改配置或消耗昂贵算力。接入同步令牌模式后,每个变更类请求都必须携带服务端下发的令牌,伪造请求直接被拒。

具体案例:某企业的AI开发平台支持在浏览器里“一键部署新模型”。攻击者把恶意页面上传到公开空间,页面里藏了一个自动提交的POST表单,目标是该平台的部署接口。未加Token前,工程师只要打开恶意页面,部署任务就被莫名触发;加上Token校验后,恶意页面拿不到隐藏字段里的合法令牌,请求返回403。

大模型应用的会话接口防伪

阶段:大模型应用开发与上线

解决的问题:基于大模型开发的应用(聊天机器人、AI绘画)常以Cookie维护用户会话,用户登录后可调用计费接口。若不加CSRF防护,攻击者可构造img或表单,诱导用户浏览器消耗积分、触发付费生成。双重提交方案适合这类无状态、多实例部署的AI应用。

具体案例:一个AI绘画小程序,用户登录后消耗点数生成图片。攻击者在论坛发帖,帖子里的img标签指向“生成一张图”的GET接口。Token防护上线后,GET请求不再直接产生业务效果,且写操作必须通过携带Token的表单或请求头完成,恶意触发从此失效。

AI数据标注平台的纵深防御

阶段:数据平台架构设计

解决的问题:数据标注平台用Kubernetes水平扩展,Session难以跨实例共享,使用双重提交方案免去服务端存储,再叠加SameSite=Lax和Origin白名单,形成多层防线。

具体案例:标注平台接受前端提交的标注结果,接口部署在多个Pod后面。开启SameSite=Lax后,外部站点发来的标注提交请求不再携带会话Cookie;同时在网关层校验Origin白名单,双重保障。即使某一层被绕过,另一层仍能拦截。

Python 演示

python49 行
import hashlibimport hmacimport secretsimport time  class CSRFProtector:    """模拟 CSRF Token 防护机制(同步令牌模式 + 双重提交方案)"""     def __init__(self, secret_key):        self.secret_key = secret_key.encode("utf-8")        self.session_tokens = {}     def generate_token(self, session_id):        # 使用密码学安全随机数 + HMAC-SHA256 生成不可预测令牌        raw = secrets.token_bytes(32)        message = session_id.encode("utf-8") + raw + str(time.time()).encode("utf-8")        token = hmac.new(self.secret_key, message, hashlib.sha256).hexdigest()        self.session_tokens[session_id] = token        return token     def verify_token(self, session_id, submitted_token):        expected = self.session_tokens.get(session_id, "")        # 恒定时间比较, 防止时序攻击        return hmac.compare_digest(expected, submitted_token)     def verify_double_submit(self, cookie_token, param_token):        # 双重提交方案: 参数令牌必须与 Cookie 令牌一致        return hmac.compare_digest(cookie_token, param_token)  def main():    protector = CSRFProtector("my-webapp-secret-key")     # 场景一: 同步令牌模式    session = "user-2024-001"    token = protector.generate_token(session)    print("[同步令牌] 页面下发Token:", token[:16], "...")    print("[同步令牌] 攻击者伪造请求:", protector.verify_token(session, "forged-token"))    print("[同步令牌] 正常表单提交:", protector.verify_token(session, token))     # 场景二: 双重提交方案    cookie_token = token    print("[双重提交] 参数被篡改:", protector.verify_double_submit(cookie_token, "evil-token"))    print("[双重提交] 参数一致:", protector.verify_double_submit(cookie_token, cookie_token))  if __name__ == "__main__":    main()
目的:实现一个极简 CSRF 防护器——用 HMAC-SHA256 为每个会话生成不可预测的令牌,并演示同步令牌模式与双重提交方案两种校验逻辑。
  1. raw = secrets.token_bytes(32)从系统安全随机源取 32 字节,保证令牌不可预测,相当于取款牌的防伪纹理。
  2. token = hmac.new(self.secret_key, message, hashlib.sha256).hexdigest()用 HMAC-SHA256 对“会话ID+随机数+时间戳”签名,生成服务端可验证的令牌字符串。
  3. self.session_tokens[session_id] = token把令牌绑定到用户会话,这就是柜员留存的“存根”。
  4. hmac.compare_digest(expected, submitted_token)恒定时间比较,避免用 == 在字符串前缀匹配时产生时间差,封死时序侧信道。
  5. verify_double_submit(cookie_token, param_token)双重提交方案只比对 Cookie 值与参数值,无需服务端保存 Session,适合无状态架构。

常见误区

误区很多初学者把HTTPS当成“万能安全”,以为网站挂了绿锁CSRF就攻击不了了。其实HTTPS加密的是传输层,请求该发还是照样发。

正解CSRF的本质是浏览器自动携带凭证,跟明文还是加密无关。即使全站HTTPS,攻击者照样可以让用户的浏览器向 https://bank.com 发出带Cookie的请求。这就像把大门换成了金门,但门禁卡还是自动被助理刷——换门锁不解决“自动刷卡”的问题。

为何常见:安全学习路径通常先接触“HTTPS加密保护数据”的说法,容易把传输安全误当成应用安全,忽略了CSRF利用的是身份验证机制的缺陷。

误区认为只要Cookie里放了Token,CSRF就防御住了,不需要在表单参数或请求头里再放一份。

正解浏览器会自动携带Cookie,服务器如果只认Cookie里的Token,等于没有加任何第二层验证——攻击者的伪造请求同样会自动带上Cookie令牌。真正的双重提交必须是:Cookie放一份,请求参数或请求头再放一份,服务端比较两者一致。攻击者跨域读不到Cookie,自然无法伪造出参数里那份副本。

为何常见:没有理解CSRF的成因是“自动携带Cookie”,把“放Token”当成了目的,而忽略了“服务端校验两个副本”这个真正的关键动作。

误区部署了Token后,认为SameSite、Origin校验都是多余的,一个方案包打天下。

正解安全需要纵深防御。Token可能因为站点存在XSS漏洞而被窃取,校验代码可能有实现缺陷,双重提交也可能因子域名失守而失效。SameSite和Origin是浏览器层和服务端层各自独立的前置防线,即使Token防线被突破,它们还能延缓和阻止攻击。

为何常见:单点防御见效快、体验改动小,容易让人误以为“一锤定音”;而现实中的安全漏洞常常是多个小漏洞组合利用的结果。

误区认为Lax是浏览器默认值就绝对安全,或者认为Strict一定最优,可以无脑上。

正解Lax会让跨站的POST请求不带Cookie,但顶层导航(如用户点击链接跳转)仍会带Cookie,所以如果某些接口用GET就能改数据,Lax也拦不住。Strict最严格,但会牺牲用户体验——从邮件、GitHub等外部链接跳转进来的用户会先看到“未登录”状态。需要按业务场景取舍,不能一刀切。

为何常见:不了解浏览器在三种取值下的具体行为差异,把“默认值”简单等同于“最佳值”。

误区觉得Token和短信验证码一样,必须每次请求都换新的,否则就不安全。

正解当页面通过Ajax局部更新时,如果每个请求都刷新Token,用户在页面上停留稍久,后续提交就会因令牌不一致而莫名失败。业界常见做法是:Token绑定会话、可复用到会话结束,只在敏感操作(改密、转账、权限变更)时轮换刷新。核心是“不可预测 + 绑定会话 + 服务端校验”,频繁刷新是增强手段而非必需。

为何常见:把“安全”简单理解成“频繁变化”,做出了牺牲可用性却无实际收益的设计。

面试题

Q1. CSRF攻击的核心本质是什么?

单选题 · easy

  • A.窃取用户Cookie中的敏感数据
  • B.利用自动携带凭证发起跨站请求← 正确答案
  • C.在服务器注入恶意SQL语句
  • D.拦截并篡改用户与服务器通信

答案:B。CSRF的本质是攻击者借用你已登录的身份发请求——就像骗子让助理自动刷卡替自己办事,而不是偷走你的卡。A属于XSS等窃取手段,C是SQL注入,D是中间人攻击,都偏离了“身份混淆”这个核心。

Q2. 下列哪项是CSRF攻击成立的必要条件?

单选题 · easy

  • A.目标站点使用HTTPS协议
  • B.用户已登录且浏览器保留会话Cookie← 正确答案
  • C.攻击者与用户处于同一局域网
  • D.目标站点使用POST接收数据

答案:B。CSRF成立的前提是你在目标站点有有效会话且Cookie被浏览器保留,这样伪造请求才能被服务器当成“你”。服务器是否用HTTPS、是否在同一局域网、是否用POST,都既非必要也非充分。

Q3. 同步令牌模式中,Token最合适的存放位置是?

单选题 · mid

  • A.客户端浏览器的本地存储
  • B.服务器端的用户Session← 正确答案
  • C.经过URL查询参数提交
  • D.浏览器扩展插件的存储

答案:B。同步令牌模式要求Token存于服务端Session,与用户会话绑定,这样柜员才有“存根”可对。客户端本地存储、URL参数、浏览器插件都无法作为服务端校验的可信依据。

Q4. 双重提交方案的核心思想是什么?

单选题 · mid

  • A.Cookie中存放两个不同的Token
  • B.参数Token与Cookie Token比对← 正确答案
  • C.浏览器同时提交两个站点的Cookie
  • D.服务端生成两个Token加密传输

答案:B。双重提交的核心是两个副本的比对——Cookie一份,表单或请求头一份,服务端只看二者是否一致。它不需要Session,适合无状态服务。攻击者跨域读不到Cookie,自然伪造不出参数里的副本。

Q5. 关于SameSite Cookie属性,下列说法正确的有?

多选题 · mid

  • A.SameSite=Strict时跨站请求都不携带Cookie← 正确答案
  • B.SameSite=Lax允许部分跨站导航请求带Cookie← 正确答案
  • C.SameSite属性可以完全替代CSRF Token
  • D.SameSite=Strict会阻止跨站发送Cookie← 正确答案

答案:ABD。Strict下所有跨站请求都不带Cookie,Lax允许顶层导航的跨站请求带Cookie,A、B、D正确。但SameSite只是浏览器侧防线,老旧浏览器不兼容,不能替代服务端Token,C错误。

Q6. Token生成时必须满足的最关键安全要求是什么?

单选题 · mid

  • A.与用户名建立明确的绑定关系
  • B.不可预测且有足够的随机熵← 正确答案
  • C.使用MD5哈希算法保证强度
  • D.长度必须固定为32个字符

答案:B。Token最核心的安全属性是不可预测。若攻击者能离线枚举,绑定关系再紧密也白搭。MD5不应用作安全随机,固定长度也不是安全保证。

Q7. 以下哪些属于有效的CSRF防御手段?

多选题 · hard

  • A.Origin头白名单校验← 正确答案
  • B.Referer头校验← 正确答案
  • C.浏览器的同源策略限制
  • D.服务端同步Token校验← 正确答案

答案:ABD。Origin白名单、Referer校验、服务端同步Token都是成熟的CSRF防御手段。同源策略是浏览器的网页隔离机制,它并不能阻止CSRF——跨站请求照样发得出去,只是读取响应被拦截。

Q8. 当浏览器请求中不带Origin头时,合理的防御策略是什么?

单选题 · hard

  • A.直接信任请求并继续处理
  • B.用Referer头兜底配合白名单← 正确答案
  • C.强制所有请求携带自定义头
  • D.放弃Origin校验改为Cookie加密

答案:B。Origin缺失时,Referer是最接近的来源证据,应配合白名单兜底。“直接信任”等于裸奔;“强制自定义头”需要服务端实际校验才有意义,否则形同虚设;“Cookie加密”与CSRF防御无关。

Q9. Token校验时防止时序攻击,应使用哪种比较方式?

单选题 · easy

  • A.使用等号直接比较字符串
  • B.恒定时间的安全比较函数← 正确答案
  • C.先转小写再比较字符串
  • D.使用MD5摘要后比较长度

答案:B。用 == 比较字符串时,遇到不一致的前缀会提前返回,产生可观测的时间差,攻击者可据此逐字符猜测Token。恒定时间比较让所有输入消耗相同时间,封死这个侧信道。

Q10. 关于CSRF Token的存储与使用,下列做法正确的有?

多选题 · hard

  • A.Token绑定会话且可跨请求复用← 正确答案
  • B.敏感操作(改密转账)时刷新Token← 正确答案
  • C.将Token放进URL查询参数提交
  • D.Token由前端JS localStorage生成

答案:ABE。Token应绑定会话并可复用,敏感操作时轮换刷新,且一律由服务端生成并存Session。放进URL会被日志和书签泄漏;由前端JS生成则等于把取款牌交给攻击者可影响的脚本控制。