安全地顯示使用者產生的 HTML 是常見的安全性挑戰。原生 Sanitizer API 提供內建且經過瀏覽器最佳化的方式,可在危險內容 (例如 <script> 標記或 on* 屬性) 進入 DOM 之前將其移除。
用戶端清理功能可保護轉譯內容的網頁。如果相同的使用者原創內容也會在其他位置 (其他用戶端、伺服器轉譯網頁或電子郵件) 轉譯,請在伺服器上進行清理,並採取深度防禦措施,確保伺服器和用戶端清理規則不會衝突。
1. 使用 setHTML() 安全插入 HTML
使用 Sanitizer API 的主要方式,是透過任何 Element 上的 setHTML() 方法。這個方法會一次完成剖析、清理和插入,確保內容會根據插入位置進行清理。
const untrustedHTML = `
<p>Hello!</p>
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<div onclick="doBadThing()">Click me</div>
`;
const container = document.getElementById('output');
// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);
2. 使用自訂清理工具設定
您可以建立 Sanitizer 執行個體,自訂清理規則。這樣一來,您就能根據特定用途,精確定義允許的元素和屬性。
使用 Element.setHTML() 和 Document.parseHTML() 時,預設的 Sanitizer 設定會移除所有 XSS 不安全的元素和屬性。您無法建立更寬鬆的 Sanitizer 來允許這些元素,但可以進一步限制允許的元素。
// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
elements: ["p", "b", "i", "strong", "em"],
attributes: ["class"],
replaceWithChildrenElements: ['div']
};
const mySanitizer = new Sanitizer(config);
// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });
不在 elements 中的元素會連同其內容一併移除。在 replaceWithChildrenElements 中列出要保留文字的包裝函式元素 (例如上方的 div),這會移除元素,但保留其子項。
如果需要更寬鬆的 Sanitizer,允許某些 XSS 不安全的元素或屬性,可以使用自訂 Sanitizer 搭配 Element.setHTMLUnsafe() 和 Document.parseHTMLUnsafe()。
3. 使用 Document.parseHTML() 剖析使用者輸入內容
如需剖析不受信任的 HTML,但不想立即插入實際 DOM,請使用 Document.parseHTML()。這個方法會傳回已根據預設或自訂規則清理的 Document,因此您可以在插入清理過的節點前,先檢查或轉換這些節點 (例如建立預覽畫面或擷取文字)。
將產生的節點移至頁面,而不是將其序列化為字串,並使用 innerHTML 重新剖析,這可能會重新引入變異 XSS (mXSS)。
const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';
const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);
Sanitizer API 的備用方案和瀏覽器支援
如果目標瀏覽器不支援原生 Sanitizer API,請在缺少原生支援時載入 DOMPurify 等程式庫,避免將程式庫傳送至不需要的瀏覽器。使用 in 檢查功能偵測,讓檢查本身在舊版瀏覽器中運作。
let domPurifyPromise;
function loadDOMPurify() {
// Code-split with your bundler so DOMPurify is only downloaded when needed
domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
return domPurifyPromise;
}
export async function safeSetHTML(el, html) {
const elements = ['p', 'b', 'i', 'strong', 'em'];
const attributes = ['class'];
if ('setHTML' in Element.prototype) {
// setHTML() accepts a plain configuration object as the sanitizer
el.setHTML(html, { sanitizer: { elements, attributes } });
} else {
const DOMPurify = await loadDOMPurify();
el.innerHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [...elements, '#text'],
ALLOWED_ATTR: attributes,
// Match the native Sanitizer: strip aria-*/data-* and disallowed element content
ALLOW_ARIA_ATTR: false,
ALLOW_DATA_ATTR: false,
KEEP_CONTENT: false
});
}
}
根據預設,DOMPurify 會保留 aria-* 和 data-* 屬性 (即使指定 ALLOWED_ATTR 也一樣),以及移除元素的文字內容,而原生 Sanitizer 則會移除 aria-* 和 data-* 屬性,並移除不允許的元素及其內容。設定 ALLOW_ARIA_ATTR: false、ALLOW_DATA_ATTR: false、KEEP_CONTENT: false 並允許 #text,可讓 DOMPurify 符合原生行為。DOMPurify 沒有與 replaceWithChildrenElements 相當的內建選項,因此在需要相符備用方案的設定中,請避免使用該選項。
如果網頁強制執行可信任類型 (require-trusted-types-for 'script'),將字串指派給 innerHTML 會擲回錯誤。setHTML() 不需要 Trusted Types 政策,但備用方案需要:將 RETURN_TRUSTED_TYPE: true 傳遞至 DOMPurify.sanitize(),並在 trusted-types CSP 指令中允許其 dompurify 政策名稱。