首页/新闻资讯/正文详情

Vue安全渲染:用DOMPurify清洗v-html防XSS注入

发布时间:2026/9/26 13:08:51 来源:云帆数科 栏目:资讯中心
Vue安全渲染:用DOMPurify清洗v-html防XSS注入
前阵子有个后台项目富文本编辑器输出的内容被直接塞进了v-html结果安全同事找上门来。让服务端返回的 HTML 在页面里安全渲染向来是 Vue 开发绕不开的一环。VueDOMPurifyHTML 这套思路说白了就是给v-html前面加一道“安检门”用成熟的 DOMPurify 库把不可信的 HTML 清洗一遍再交给浏览器渲染。这篇我把自己平时用的基础写法、配置技巧和踩过的坑整理出来适合正在用 Vue2/Vue3 处理富文本、活动页配置、接口返回 HTML 内容的同学参考。1. 先聊聊 v-html 和 XSS 之间的那点“不信任”1.1 一个最容易被忽略的注入入口v-html是 Vue 提供的一个指令作用很直白把字符串当成 HTML 插入到元素内部配合插值语法{{ }}不能渲染标签的问题特别有用。但它的风险也出在这里它不会对内容做任何过滤字符串里的script、onerror、javascript:这类东西会被浏览器原封不动地执行。我见过一个典型例子某管理后台让运营在文本框里配置活动公告后端原样存库前端用v-html展示。运营误贴了一段带img onerror的代码结果每次图片加载失败都会触发脚本请求整个页面被拉去下载了一段外部资源。这种问题在字段多了以后很难靠人工审查发现。从安全的视角看这属于典型的存储型 XSS。攻击者不一定直接改你的页面文件而是通过修改业务数据让恶意脚本在别人浏览器里跑起来。这类漏洞最难防因为受害者打开的是一个完全正常的后台地址数据来自可信域名下的接口v-html也不会在控制台报警告。1.2 自己写正则过滤为什么不靠谱不少人第一反应是我用正则把script标签、onerror属性替换掉不就完了我最初也这么干过后来发现这是一个无底洞。问题在于 HTML 的解析规则远比正则表达式复杂。scrscriptipt、svgscript、属性值里的大小写变形、HTML 实体编码后的#106;avascript:甚至不带引号的属性写法都能绕过简单过滤。你正则写得越多攻击者绕过的手法就越刁钻。DOMPurify 的定位不是“正则补丁”而是一个基于真实 DOM 解析的净化器。它会先把 HTML 解析成 DOM 节点把脚本、事件属性、危险 URI 识别出来并移除再把安全的节点序列化回字符串。解析是在浏览器自己的 DOM 引擎里进行的和浏览器实际渲染时的理解一致因此很难出现“过滤时认为是文本、渲染时变成标签”的绕过情况。像 VueDOMPurifyHTML 这种组合就是把 DOMPurify 作为处理核心在 Vue 指令层做接入既保留v-html的便利又把风险挡在外面。2. 基础安装与环境准备先说结论再动手2.1 安装 DOMPurify 就是这么简单DOMPurify 是独立于 Vue 的库你不需要任何 Vue 插件直接用 npm 或 yarn 装就行。npm install dompurify # 或 yarn add dompurify如果你的项目是老式 script 标签引入方式也可以通过 CDN 加载script srchttps://cdn.jsdelivr.net/npm/dompurify3/dist/purify.min.js/script装完后在 Vue 组件里引入即可import DOMPurify from dompurify;需要提醒一句如果用的是 Nuxt 或 SSR 场景直接import DOMPurify在服务端会报错因为它默认依赖浏览器环境。后面章节我会专门讲 SSR 下的处理方案。2.2 先封装一个安全的渲染工具函数我建议不要在每个组件里都写DOMPurify.sanitize而是统一封装成一个工具函数方便加日志、改配置。新建一个utils/safeHtml.jsimport DOMPurify from dompurify; const defaultConfig { ALLOWED_TAGS: [ p, br, strong, b, em, i, u, h1, h2, h3, h4, ul, ol, li, a, img, span, div, blockquote, code ], ALLOWED_ATTR: [href, title, target, rel, src, alt, class], ALLOWED_URI_REGEXP: /^(?:(?:https?|ftp):|mailto:|tel:)/i }; export function safeHtml(dirtyHtml, options {}) { const config Object.assign({}, defaultConfig, options); return DOMPurify.sanitize(dirtyHtml, config); }这里有几个关键配置我想展开说ALLOWED_TAGS: 声明哪些标签可以保留。不在列表里的标签会被直接移除。ALLOWED_ATTR: 声明标签上允许哪些属性。像style、onmouseover这类属性默认会被摘掉。ALLOWED_URI_REGEXP: 限制href、src这类属性里允许的协议。比如我保留了mailto:和tel:只允许 http/https/ftp避免javascript:伪协议。封装好以后你在业务代码里只要调safeHtml(dirtyContent)就行后续要改白名单只需要改这一处。3. 在 Vue 里写一个 v-safe-html 指令把净化逻辑收敛到一处3.1 自定义指令的核心逻辑有了工具函数有人会问我直接在 template 里写v-htmlsafeHtml(content)不也可以吗确实可以但有两个问题。一方面组件里每个用到的地方都要引入safeHtml代码冗余而且很容易漏改。另一方面Vue 的模板表达式是在当前组件作用域里求值的如果内容里包含多层引号、拼接可读性会比较差。更优雅的做法是注册一个自定义指令v-safe-html把净化和赋值逻辑收敛到指令里。以 Vue3 的写法为例import DOMPurify from dompurify; const safeHtmlDirective { mounted(el, binding) { el.innerHTML DOMPurify.sanitize(binding.value, { ALLOWED_TAGS: [p, br, strong, em, i, a, img, ul, ol, li, span, div], ALLOWED_ATTR: [href, target, rel, src, alt, class, title] }); }, updated(el, binding) { if (binding.value ! binding.oldValue) { el.innerHTML DOMPurify.sanitize(binding.value); } } }; export default { install(app) { app.directive(safe-html, safeHtmlDirective); } };这里有两个细节值得注意。updated钩子里我判断了binding.value和binding.oldValue是否相同避免每个响应式更新都重复做 sanitize。虽然 DOMPurify 性能已经很好但能省则省。其次el.innerHTML ...这个操作是主动覆盖不需要担心 Vue 的虚拟 DOM 和指令里直接操作 innerHTML 会打架因为指令的mounted和updated执行时机在 Vue 完成 DOM 操作之后。3.2 在组件里怎么用注册完指令后使用方式和平时的v-html几乎一样template div classarticle-content v-safe-htmlcontent/div /template script setup import { ref } from vue; const content ref(pHello strongVueDOMPurifyHTML/strong/pscriptalert(1)\/script); /script如果项目里用的是 Options API直接注册局部指令import DOMPurify from dompurify; export default { directives: { safe-html: { mounted(el, binding) { el.innerHTML DOMPurify.sanitize(binding.value); } } }, data() { return { content: p富文本内容/p }; } };我在实际项目里推荐用全局指令因为后台系统里“根据字段渲染 HTML 预览”的场景太多了全局注册后新同学写页面时不需要额外 import只要记得这里有安全检查。这个自定义指令的价值在于你把所有关于“脏 HTML”的处理统一收归到一个指令里代码审计时只需盯着这一个文件排查成本低很多。同时业务侧使用成本几乎没有增加这在实际团队协作中非常重要。4. DOMPurify 的配置项按需求收紧松绑4.1 常用配置速查DOMPurify 的配置项很多但日常开发里真正高频用到的就那几个。我整理了一张表方便你按需对照。配置项作用使用示例ALLOWED_TAGS白名单标签不在名单里的会被移除ALLOWED_TAGS: [p, a, img]ALLOWED_ATTR白名单属性ALLOWED_ATTR: [href, src, alt]FORBID_TAGS黑名单标签优先于白名单FORBID_TAGS: [style, form, input]FORBID_ATTR黑名单属性FORBID_ATTR: [style, onerror, onclick]USE_PROFILES快速按场景切换白名单{ html: true }或{ svg: true }或{ svgFilters: true }ALLOW_DATA_ATTR是否保留>import DOMPurify from dompurify; const config { ALLOWED_TAGS: [h1, h2, h3, p, br, img, a, ul, ol, li, strong, em, blockquote], ALLOWED_ATTR: [href, src, alt, title, target], FORBID_TAGS: [style, script, iframe, object, embed, form, input], ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|tel:)/i, ALLOW_DATA_ATTR: false };这里FORBID_TAGS和ALLOWED_TAGS同时存在不冲突。白名单是“第一道闸门”黑名单是“补充拦截”两者组合能减少漏网之鱼。4.2 自定义 hook在净化前后做点私活DOMPurify 还提供了hooks机制。比如我遇到过一个场景用户上传的图片不受信任我要在净化过程中自动给图片添加referrerpolicyno-referrer来防止某些安全扫描器告警。import DOMPurify from dompurify; DOMPurify.addHook(afterSanitizeAttributes, function (node) { if (node.tagName IMG) { node.setAttribute(referrerpolicy, no-referrer); node.setAttribute(loading, lazy); } if (node.tagName A) { node.setAttribute(rel, noopener noreferrer); } });afterSanitizeAttributes在 DOMPurify 对节点属性净化完成后触发此时恶意属性和协议已经被移除你做的补充是相对安全的。如果你在beforeSanitizeAttributes里做操作就得非常小心因为节点可能还带着危险属性。这个 hook 对不同项目非常实用。比如我做过一个数据可视化项目用户上传的 SVG 需要保留固定属性但又要确保里面没有脚本就是通过 hook 在净化后统一补充xmlns和viewBox属性实现的。5. 业务里的两个典型接入场景富文本编辑器和 Markdown 渲染5.1 富文本编辑器内容入库前还是渲染时净化团队里经常争论一个问题净化到底放在前端提交时还是渲染时我的看法是渲染时净化比入库前净化更重要。原因很现实数据库里的旧数据可能没有经过新规则清洗某个接口的数据可能是另一个团队写入的甚至你无法控制上游数据。你在渲染边界做净化等于最后一道防线覆盖所有来源。但我也建议在提交时做一次“轻量提示”。比如用 Quill 这类富文本编辑器时用户粘贴进来的带格式内容可能包含大量奇奇怪怪的 class。提交前你可以把safeHtml后的结果返回给前端做对比如果前后不一致说明内容里被过滤了东西可以给运营一个确认提示。这样隐私性和体验都照顾到了。一个小提醒不要在编辑器初始化时全局替换原生粘贴。编辑器内部有自己的粘贴过滤逻辑直接改剪贴板 HTML 反而会破坏编辑器的功能。5.2 和 Markdown 渲染器搭配使用时净化时机要放在最后很多社区项目用markdown-it渲染 Markdown。markdown-it本身只负责把 Markdown 转成 HTML它不处理 XSS 风险。正确做法是让markdown-it先生成 HTML再交给 DOMPurify 净化。import MarkdownIt from markdown-it; import DOMPurify from dompurify; const md new MarkdownIt({ html: false, linkify: true }); export function renderMarkdown(text) { const rawHtml md.render(text || ); return DOMPurify.sanitize(rawHtml, { ALLOWED_TAGS: [p, br, strong, em, code, pre, a, ul, ol, li, blockquote, h1, h2, h3, img], ALLOWED_ATTR: [href, src, alt, title, target] }); }html: false是一个关键选项。它让 markdown-it 不解析内嵌原始 HTML从源头减少脏输入。但仍要补 DOMPurify因为linkify: true会生成链接攻击者可能用形如[x](javascript:alert(1))的语法构造危险链接。DOMPurify 里配置的ALLOWED_URI_REGEXP就是为了防这个。实际项目里我发现一个高频问题Markdown 里的class通常不需要保留我们在配置里直接不给class放行可以规避一大批样式相关漏洞。如果运营想要代码高亮建议把高亮样式做在自己的 CSS 类里而不是让用户内联样式进来。5.3 服务端渲染场景到底怎么办前面提到 Nuxt SSR 下直接import DOMPurify会报错。原因是 DOMPurify 默认使用window服务端没有 DOM 环境。有两个解法。一种是用.isomorphic方案安装isomorphic-dompurify它在服务端会调用 jsdom 模拟 DOMnpm install isomorphic-dompurifyimport DOMPurify from isomorphic-dompurify; export function safeHtml(value) { return DOMPurify.sanitize(value, config); }另一种是仅在客户端渲染时净化。比如文章详情页首次 SSR 输出原始 HTML虽然不符合安全预期但配合ClientOnly组件在客户端完成渲染前净化也可以。只是这样会影响 SEO 和首屏我更推荐前者。需要留意的是isomorphic-dompurify在服务端依赖jsdom体积偏大部署环境如果有内存限制要注意构建产物大小。如果项目只在客户端用 Vue那直接用dompurify就够了不必引入 isomorphic 版本。6. 我踩过的一些坑和现在建议的默认参数6.1 常见报错和边界情况DOMPurify 用起来整体稳定但踩坑还是难免。我把高频问题整理成了速查表。现象可能原因解决方案空白内容被净化为空字符串白名单里没包含文本节点外层标签或 KEEP_CONTENT 设成 false确认需要的标签在白名单文本内容一般在 p/span/div 里服务端渲染时window is not defined直接用了不兼容 SSR 的包换成 isomorphic-dompurify 或延迟到客户端处理href里的#或相对链接被删掉ALLOWED_URI_REGEXP限制了协议#不是合法 http 协议正则里补上相对链接/^(?:(?:https?图片能显示但srcbase64 被过滤默认不允许 base64 URI在 ALLOWED_URI_REGEXP 里加data:image\/匹配富文本里的行内样式丢失默认 ALLOWED_ATTR 没有 style如果确有强需求单独给 style 配ALLOWED_ATTR: [style]并用 CSS 属性白名单 hook 进一步收紧DOMPurify 版本升级后行为变化3.x 对某些 URI 正则处理更严格升级前看 Release Note回退旧版本前要评估安全风险这里特别说一下KEEP_CONTENT。它控制当某个标签被移除时标签内部的文本要不要保留。例如你禁止了h1但允许p如果KEEP_CONTENT为真h1标题/h1会变成文本“标题”如果为假连标题文字一起消失。这个配置很容易被忽略但业务感知非常强烈。6.2 性能优化不要每次都重新净化大段内容DOMPurify 处理普通富文本的速度很快但在长文章、评论列表、大量卡片渲染的页面上如果每个 cell 都反复 sanitize还是会有些开销。我常用的优化思路有三个。第一能缓存就缓存。如果同一个内容在页面里多次展示用Map做一层内存缓存key 是原始字符串value 是净化结果。const sanitizeCache new Map(); export function safeHtmlCached(dirtyHtml, config) { if (sanitizeCache.has(dirtyHtml)) { return sanitizeCache.get(dirtyHtml); } const clean DOMPurify.sanitize(dirtyHtml, config); sanitizeCache.set(dirtyHtml, clean); return clean; }这个做法只适合纯字符串渲染场景例如评论列表、商品详情。如果内容依赖当前用户状态或服务端返回个性化数据就不适合缓存。第二按需净化不要对整段内容一刀切。如果富文本来自可信配置表里面的脚本已经被编辑后台控制住了可以只对用户生成内容UGC部分做严格净化静态配置部分直接渲染。这种分层策略能显著降低净化耗时。第三推迟渲染。对可视化区域之外的内容可以用懒加载或虚拟列表来减少初始渲染时的净化数量。用户滚动时再处理当前可见项实际上每个项目的净化成本就被分散了。6.3 我个人的默认配置和工作习惯在不同项目里折腾下来我现在的默认配置大致是这样const baseConfig { ALLOWED_TAGS: [ p, br, strong, b, em, i, u, h1, h2, h3, h4, h5, h6, ul, ol, li, a, img, span, div, blockquote, code, pre, table, thead, tbody, tr, th, td, hr ], ALLOWED_ATTR: [ href, src, alt, title, target, rel, colspan, rowspan ], ALLOWED_URI_REGEXP: /^(?:(?:https?|ftp):|mailto:|tel:|#|\/)/i };我不会默认开放style和class。如果业务方明确要求某些富文本样式保留再单独在ALLOWED_ATTR里放开并对 style 属性里的内容做额外校验。因为一旦开放 styleCSS 注入虽然不如 JS 注入那么直接但可能造成钓鱼页面覆盖比如给某个按钮写上position: fixed; top: 0; width: 100%; height: 100%把整个站点的点击区域遮住。在做代码 review 时我建议把safeHtml工具函数和v-safe-html指令列为重点审查对象。团队新成员不一定熟悉 XSS 注入手法但他们会天然信任“过滤函数”。因此我在工具函数里加了日志逻辑当净化前后结果不一致时把原始内容打印到控制台并上报到一个内部统计接口。这个日志的价值在于你能提前发现运营或用户正在输入哪些可疑内容而不是等到出事了再溯源。最后分享一个小技巧如果你在调试某个富文本为什么被净化成空白可以先单独在浏览器控制台调用DOMPurify.sanitize(html, config)对比结果逐步调整配置。这个方式比反复刷新页面高效得多。DOMPurify 还有个官方 demo 页面可以实时测试配置效果拿它做回归验证会比自己在项目里盲试舒服。我个人现在每个涉及富文本的项目都先跑一遍 demo 集测试用例再落到代码里省了不少来回调试的时间。

相关推荐

RAG+Wiki自进化:腾讯开源WeKnora企业知识库搭建实测指南
RAG+Wiki自进化:腾讯开源WeKnora企业知识库搭建实测指南

做企业知识库三年,我最大的体会是:真正难的往往不是找到一个大模型,而是把文档接入、解析、切分、检索、问答、知识沉淀这一整条链路打通。今天要聊的 WeKnora,来自腾讯开源阵营,主打 RAG 问答和 Wiki 自进化&#xff… · 2026/9/26 13:08:51

MBO优化BP神经网络的光伏功率预测MATLAB实现与GUI设计
MBO优化BP神经网络的光伏功率预测MATLAB实现与GUI设计

做光伏功率预测,恐怕是新能源从业者绕不开的硬骨头。气象波动、数据噪声、模型参数敏感,随便一个环节没处理好,预测精度就垮得很难看。这个项目选用MATLAB做开发环境,用蜜獾优化算法(MBO)去优化BP神经网络的… · 2026/9/26 13:08:51

CLI才是王道:OpenClaw与InfiniSynapse的共识——TaoToken统一Key接入实战
CLI才是王道:OpenClaw与InfiniSynapse的共识——TaoToken统一Key接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:08:51

应用日语毕业论文,别一上来就问“哪个AI最强”[特殊字符]
应用日语毕业论文,别一上来就问“哪个AI最强”[特殊字符]

先把场景说具体:假设你是教育与体育大类 / 语言类 / 应用日语专业的学生,正在做毕业论文,题目类似《日系酒店前台服务中的敬语误用研究——基于实习访谈与问卷的分析》。 这类题目的难点很典型: 要查中文和日文两类资料&#xf… · 2026/9/26 13:40:49

AAMAS投稿全指南:多智能体系统学术圣殿的准入逻辑
AAMAS投稿全指南:多智能体系统学术圣殿的准入逻辑

1. AAMAS不是“AI会议”而是多智能体系统的学术圣殿:先破除三个常见误解很多人第一次听说AAMAS,是在某篇论文的参考文献里看到缩写,或者在导师随口一句“这个方向投AAMAS比较对口”中偶然撞见。更常见的是,在中文社区里被笼统地归… · 2026/9/26 13:40:49

从一只蓝牙耳机充电盒开始:电子产品检测人的毕设 AI 搭子怎么选
从一只蓝牙耳机充电盒开始:电子产品检测人的毕设 AI 搭子怎么选

电子产品检测技术专业的同学,大概都懂这种感觉:一只看起来很小的 TWS 蓝牙耳机充电盒,真做成毕业项目时,事情一点也不少。 它里面有锂电池、充电管理电路、接口、外壳和保护器件。你可能要完成的任务是:制定一份“蓝牙… · 2026/9/26 13:40:42

AI电子元器件行业解决方案:从选型到量产,拆解落地路径与避坑指南
AI电子元器件行业解决方案:从选型到量产,拆解落地路径与避坑指南

电子元器件这个行当,过去二十年拼的是渠道、库存和交期。但这两年跟不少做采购、做FAE、做供应链的朋友聊下来,大家共同的感受是:光靠"关系经验"已经不够用了。一颗料从选型到量产,中间牵扯的数据量、文档量、替代料判断… · 2026/9/26 13:40:42

CUDA与NVIDIA驱动版本不匹配?一文讲清版本对应关系与排查方法
CUDA与NVIDIA驱动版本不匹配?一文讲清版本对应关系与排查方法

1. 为什么CUDA和驱动版本对不上会让你抓狂如果你折腾过深度学习环境,大概率遇到过这种场景:兴冲冲地装好了PyTorch,torch.cuda.is_available()却冷冰冰地返回False;或者跑一个开源项目,上来就报CUDA error: no kernel … · 2026/9/26 13:40:42

WorkBuddy实战:从大模型到AI Agent,四十分钟完成网站发布
WorkBuddy实战:从大模型到AI Agent,四十分钟完成网站发布

这两年我明显感觉到一个变化:大家不再问“AI 能不能写代码”,而是问“AI 能不能把一件完整的事做完”。如果你现在还觉得 AI Agent 只是“更聪明的聊天机器人”,那 2026 年的效率红利基本和你没什么关系。最近我把一套“从需求到发布”的流程… · 2026/9/26 13:40:22

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码