sanitize-html
富文本白名单消毒(auto-import)。运营在管理台录入的 HTML(公告 / 活动说明 / 帮助文案)禁止裸 v-html —— 先过 sanitizeHtml。
<script setup lang="ts">
const safe = ref('')
onMounted(() => {
safe.value = sanitizeHtml(notice.data) // 🔴 client-only,见下
})
</script>
<template>
<div v-html="safe" />
</template>
API
sanitizeHtml(html: string, options?: {
allowedTags?: readonly string[] // 默认 13 个常用标签;只能取 SAFE_TAGS 子集
allowRelativeUrl?: boolean // 默认 true:a[href] / img[src] 放行站内 /path
}): string
用 DOMParser 重建 DOM(不是正则替换):非白名单标签「去壳留文」保留子内容、危险标签整块丢弃;on* / style 一律剥除;a 自动补 target="_blank" + rel="noopener noreferrer nofollow";img 自动补 loading="lazy"。
🔴 client-only
依赖 DOMParser(浏览器 API),SSR 期调用会显式抛错(而非静默返回空串 —— 静默会让「公告不显示」变成无线索的谜题)。放 onMounted / IS_CLIENT 守卫 / <ClientOnly> 内,与 decryptBinary / dbGet 同族纪律。
别为了 SSR 去手写同构的 HTML tokenizer —— 那正是「白名单窄所以手写可控」的对偶:tokenizer 宽且安全敏感,不该自己造。真需要 SSR 消毒时请提 issue 单独评估。
白名单为什么只能取 SAFE_TAGS 子集
allowedTags 可以收窄(各站按自己产品形态裁剪),但不能越过 framework 维护的 SAFE_TAGS 超集 —— 越界的标签会被忽略,dev 期 console.error 点名。
原因:本函数返回字符串,消费方 v-html 会再解析一次。parse → serialize → re-parse 正是 mXSS(mutation XSS) 的攻击面,而所有已知向量都依赖解析规则特殊的元素 —— foreign content(svg / math)、raw text(style / xmp)、escapable raw text(textarea / title)、scripting 敏感(noscript)、template 与表格 foster parenting。
SAFE_TAGS 的收录标准就是解析规则普通,一个都不沾。若开放自由传值,某个站为了让运营插表格传了 table、为了排版传了 svg,就会在毫不知情的情况下把攻击面装回来 —— 而且没有任何东西会提醒他。
要扩超集请提 framework issue,审一次全生态受益。
// ✅ 收窄:只要纯文本排版
sanitizeHtml(raw, { allowedTags: ['P', 'BR', 'B', 'STRONG'] })
// ❌ 越界:SCRIPT / TABLE 不在 SAFE_TAGS 里,会被忽略 + dev 报错
sanitizeHtml(raw, { allowedTags: ['P', 'SCRIPT', 'TABLE'] })
allowRelativeUrl
默认 true:a[href] 与 img[src] 一致地放行单 / 开头的站内路径(协议相对地址 //evil.com 与反斜杠绕过 /\evil.com 始终拒绝)。设 false 则两者都只认绝对 http(s)。
这个开关统一管 a 与 img。早期各站实现里 img 比 a 更严(理由「防路径探测」),framework 版判定该理由不成立 —— 能注入 HTML 的攻击者早有更好的探测手段,而代价是运营插站内图被静默吞掉。
注意事项
- 别在业务侧自己写消毒:消毒写错不会当场报错 —— XSS 面静默存在、测试全绿、页面正常,直到被利用。
- 别用
??之类兜底掩盖异常:若消毒后内容为空,先确认源 HTML 是否本就为空。 - 注释节点、
<head>内容(title/meta/ 前置style)一律丢弃。 - 超深嵌套会被截断(深度上限 100),不会拖垮主线程。