使用 Trusted Types 防範 DOM 型 XSS 攻擊

網頁功能: Trusted types

Trusted Types 是一項安全防護功能,可防止 DOM 型跨網站指令碼攻擊 (DOM XSS),方法是要求傳入「危險」瀏覽器 API (稱為接收器) 的資料,必須先轉換為特定類型,例如 TrustedHTML。

根據預設,許多 DOM API (例如 innerHTML 或 document.write) 都接受原始字串。如果字串含有來自不受信任來源的惡意指令碼,就可能執行。Trusted Types 會改變這項行為:透過 CSP 啟用後,這些接收器只會接受由授權政策建立的 TrustedHTML 物件。

重要導入細節

如要使用 Trusted Types,請按照下列四個主要步驟操作:

  1. 定義安全性政策,指定如何清理或轉換字串。
  2. 使用該政策,從原始字串建立 Trusted Type 物件。
  3. 寫入 DOM 接收器時,請使用 Trusted Type 物件。
  4. 使用內容安全政策 (CSP) 強制執行 Trusted Types。

1. 建立政策

政策是一組用於清理內容的規則。您可以使用 trustedTypes.createPolicy() 建立,通常會委派給淨化程式庫 (例如 DOMPurify) 或應用程式的 HTML 逸出規則:

import DOMPurify from 'dompurify';

const myPolicy = trustedTypes.createPolicy('sanitize-html', {
  createHTML: (input) =>
    // Return a sanitized string; createPolicy() wraps it in a TrustedHTML object.
    // Example using DOMPurify:
    DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
});

// Example using HTML escaping (for environments without third-party sanitizers):
const escapePolicy = trustedTypes.createPolicy('escape-html', {
  createHTML: (input) =>
    String(input)
      .replaceAll('&', '&')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;'),
});

2. 使用政策

請勿直接將字串傳遞至接收器,而是呼叫政策的建立方法。

const untrustedInput = '<p>Hello, <strong>world</strong>!</p><img src="x" onerror="alert(1)">';

// In an enforced environment, this would throw a TypeError:
// element.innerHTML = untrustedInput;

// This returns a TrustedHTML object with unsafe elements and attributes removed:
const cleanHTML = myPolicy.createHTML(untrustedInput);
// Result: "<p>Hello, <strong>world</strong>!</p><img src=\"x\">"

3. 將接收器轉換為使用可信任類型

更新寫入接收器的每個執行個體,先將其轉換為 Trusted Type 物件。如要找出需要更新程式碼的位置,您可以定義暫時的 default 政策,在接收端寫入字串而非 TrustedType 時記錄。

trustedTypes.createPolicy("default", {
  createHTML(value) {
    console.warn("String passed instead of Trusted Type.");
    return DOMPurify.sanitize(value, { RETURN_TRUSTED_TYPE: false });
  },
});

支援可信任類型的常見接收器包括:

  • HTML: element.innerHTML、element.outerHTML、document.write()、element.setHTMLUnsafe()、Document.parseHTMLUnsafe()
  • 指令碼: <script> 文字內容,eval()
  • ScriptURL: <script src>、Worker()、SharedWorker()
// Assign the TrustedHTML object.
element.innerHTML = cleanHTML;

請注意,「安全」的 HTML 清理方法 (element.setHTML() 和 Document.parseHTML(),詳情請參閱「清理不受信任的 HTML」) 不需要 Trusted Types 政策,因為這些方法一律會以原生方式清理潛在的 XSS 攻擊。

4. 使用 CSP 強制執行 Trusted Types

Trusted Types 應逐步推出,以免應用程式中斷。雖然完整強制執行可提供最強大的保護,但如果並非所有接收器寫入作業都已遷移,則可能會有網站中斷的風險。

必要條件

  • 架構合作:如果應用程式的架構 (或任何寫入 DOM 接收器的第三方程式庫) 未產生 TrustedHTML 或 TrustedScript 值,您就必須破壞該程式碼,才能強制執行可信任類型。在開始推出前,請先檢查架構支援。
  • 接收端重構:請務必找出並重構危險接收端,以便使用 Trusted Types 政策。

推出策略

  1. 僅限報表:首先,使用 Content-Security-Policy-Report-Only 標頭推出。這樣一來,系統就會將違規事項記錄到回報端點,但不會實際封鎖寫入作業。

    Content-Security-Policy-Report-Only: require-trusted-types-for 'script';
    
  2. 強制執行:整合政策後,如果違規報告數量降至零,即可全面強制執行。

    Content-Security-Policy: require-trusted-types-for 'script';
    

您也可以限制可建立的政策 (列出政策名稱,例如 sanitize-html 或 escape-html;如果政策委派給 DOMPurify.sanitize(),請加入 dompurify,這會在內部建立 dompurify 政策;如果您在遷移期間註冊預設政策,請加入 default):

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;

有了這個 CSP,任何不使用 Trusted Types 的接收器寫入作業都會擲回錯誤。此外,如果嘗試建立的 Trusted Type 政策名稱未列在 trusted-types 中,系統會擲回錯誤。

您也可以在文件的 <head> 中將此 CSP 設為 <meta> 標記,但這樣做可能較不安全。

<!-- First child of `<head>`, as earlier tags will not enforce this CSP. -->
<meta
  http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;"
/>

備用策略

如果瀏覽器尚未支援可信任類型,您可以使用最精簡的「tinyfill」程式碼,確保程式碼不會中斷,同時仍提供清理功能。

if (!window.trustedTypes) {
  window.trustedTypes = {
    createPolicy: (name, rules) => rules,
  };
}

這樣一來,您現在就能使用 Trusted Types 模式編寫程式碼。支援的瀏覽器會強制執行類型,而舊版瀏覽器仍會透過模擬政策執行清理邏輯。

如果瀏覽器不支援可信任類型,則會將 TrustedHTML 物件視為字串 (如果使用 tinyfill),或不會在接收器層級執行強制檢查。