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),不会拖垮主线程。