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),不會拖垮主執行緒。