ShadowRoot: setHTMLUnsafe() 方法

基準線 2025
新推出

自 ⁨2025 年 9 月⁩起,此功能適用於最新裝置和瀏覽器版本。此功能可能不適用於較舊的裝置或瀏覽器。

警告:此方法將其輸入解析為 HTML,並將結果寫入 DOM。此類 API 被稱為 注入槽,如果輸入最初來自攻擊者,則可能成為 跨站點指令碼 (XSS) 攻擊的載體。

您可以透過始終傳遞 TrustedHTML 物件而不是字串並 強制執行可信型別 來緩解此風險。有關更多資訊,請參閱 安全注意事項

注意: 在支援 ShadowRoot.setHTML() 的瀏覽器中,幾乎總是應該使用該方法而不是本方法,因為它始終會移除 XSS 不安全的 HTML 實體。

ShadowRoot 介面的 setHTMLUnsafe() 方法可用於將 HTML 輸入解析為 DocumentFragment,可以選擇性地過濾掉不需要的元素和屬性,然後用它來替換 Shadow DOM 中現有的樹。

語法

js
setHTMLUnsafe(input)
setHTMLUnsafe(input, options)

引數

input

定義要解析的 HTML 的 TrustedHTML 或字串例項。

options 可選

一個包含以下可選引數的 options 物件

sanitizer 可選

定義允許或移除輸入內容的元素以及屬性的 SanitizerSanitizerConfig 物件。這也可以是值為 "default" 的字串,該值應用具有預設(XSS 安全)配置的 Sanitizer。如果未指定,則不使用 Sanitizer。

請注意,如果配置需要重用,通常 Sanitizer 的效率會比 SanitizerConfig 高。

返回值

無 (undefined)。

異常

TypeError

如果出現以下情況,將丟擲此錯誤

描述

setHTMLUnsafe() 方法可用於解析 HTML 字串,可以選擇性地過濾掉不需要的元素和屬性,並用它來替換現有的 Shadow DOM。

ShadowRoot.innerHTML 不同,輸入中的宣告式 Shadow DOM 將會被解析到 DOM 中。如果 HTML 字串在特定的 Shadow Host 中定義了多個宣告式 Shadow DOM,則只會建立第一個 ShadowRoot——後續的宣告將被解析為該 Shadow DOM 內的 <template> 元素。

setHTMLUnsafe() 預設情況下不執行任何清理。如果未作為引數傳遞 Sanitizer,則輸入中的所有 HTML 實體都將被注入。

安全注意事項

方法名稱中的“Unsafe”字尾表示它不會強制移除所有 XSS 不安全的 HTML 實體(與 ShadowRoot.setHTML() 不同)。雖然如果使用適當的 Sanitizer,它可以做到這一點,但它不一定使用有效的 Sanitizer,或者根本不使用 Sanitizer!因此,該方法是跨站指令碼 (XSS) 攻擊的潛在載體,在這種攻擊中,使用者提供的潛在不安全字串在未先清理的情況下被注入到 DOM 中。

您應該透過始終傳遞 TrustedHTML 物件而不是字串,並使用 require-trusted-types-for CSP 指令強制執行受信任型別來降低此風險。這確保了輸入透過轉換函式,該函式有機會清理輸入以移除潛在危險的標記(例如 <script> 元素和事件處理程式屬性),然後才注入。

使用 TrustedHTML 可以在少數幾個地方審計和檢查清理程式碼是否有效,而不是散佈在所有注入點。使用 TrustedHTML 時,您不應該需要向方法傳遞 Sanitizer。

如果由於任何原因您無法使用 TrustedHTML(或者更好的是 setHTML()),那麼下一個最安全的選擇是使用帶有 XSS 安全預設 SanitizersetHTMLUnsafe()

何時應使用 setHTMLUnsafe()

如果 ShadowRoot.setHTML() 可用,則幾乎不應使用 setHTMLUnsafe(),因為使用者提供的 HTML 輸入很少(甚至沒有)需要包含 XSS 不安全的元素。不僅 setHTML() 是安全的,而且它還可以避免考慮受信任的型別。

使用 setHTMLUnsafe() 可能是合適的,如果

  • 您由於任何原因無法使用 setHTML() 或受信任型別,並且您希望儘可能安全地進行過濾。在這種情況下,您可以使用 setHTMLUnsafe() 和預設的 Sanitizer 來過濾所有 XSS 不安全的元素。

  • 您無法使用 setHTML(),並且輸入可能包含宣告式 Shadow DOM,因此您無法使用 ShadowRoot.innerHTML

  • 您有一個特殊情況,您必須允許包含一組已知的 XSS 不安全 HTML 實體的 HTML 輸入。

    在這種情況下,您無法使用 setHTML(),因為它會剝離所有不安全的實體。您可以使用不帶 Sanitizer 的 setHTMLUnsafe()innerHTML,但這將允許所有不安全的實體。

    這裡更好的選擇是呼叫 setHTMLUnsafe() 並使用一個只允許我們實際需要的危險元素和屬性的 Sanitizer。雖然這仍然不安全,但比允許所有這些元素和屬性更安全。

對於最後一點,請考慮一種情況,您的程式碼依賴於能夠使用不安全的 onclick 處理程式。以下程式碼顯示了不同方法和 Sanitizer 在此情況下的效果。

js
const shadow = document.querySelector("#host").shadowRoot;

const input = "<img src=x onclick=alert('onclick') onerror=alert('onerror')>";

// Safe - removes all XSS-unsafe entities.
shadow.setHTML(input);

// Removes no event handler attributes
shadow.setHTMLUnsafe(input);
shadow.innerHTML = input;

// Safe - removes all XSS-unsafe entities.
const configSafe = new Sanitizer();
shadow.setHTMLUnsafe(input, { sanitizer: configSafe });

// Removes all XSS-unsafe entities except `onclick`
const configLessSafe = new Sanitizer();
config.allowAttribute("onclick");
shadow.setHTMLUnsafe(input, { sanitizer: configLessSafe });

示例

使用受信任型別的 setHTMLUnsafe()

為降低 XSS 風險,我們將首先從包含 HTML 的字串建立 TrustedHTML 物件,然後將該物件傳遞給 setHTMLUnsafe()。由於並非所有瀏覽器都支援受信任型別,因此我們定義了受信任型別 tinyfill。它充當受信任型別 JavaScript API 的透明替代品。

js
if (typeof trustedTypes === "undefined")
  trustedTypes = { createPolicy: (n, rules) => rules };

接下來,我們建立一個 TrustedTypePolicy,它定義了一個 createHTML(),用於將輸入字串轉換為 TrustedHTML 例項。通常,createHTML() 的實現使用像 DOMPurify 這樣的庫來清理輸入,如下所示

js
const policy = trustedTypes.createPolicy("my-policy", {
  createHTML: (input) => DOMPurify.sanitize(input),
});

然後,我們使用此 policy 物件從潛在不安全的輸入字串建立 TrustedHTML 物件。

js
// The potentially malicious string
const untrustedString = "abc <script>alert(1)<" + "/script> def";
// Create a TrustedHTML instance using the policy
const trustedHTML = policy.createHTML(untrustedString);

現在我們有了 trustedHTML,下面的程式碼顯示瞭如何將其與 setHTMLUnsafe() 一起使用。首先,我們建立要定位的 ShadowRoot。這可以使用 Element.attachShadow() 以程式設計方式建立,但在此示例中,我們將以宣告方式建立根。

html
<div id="host">
  <template shadowrootmode="open">
    <span>A span element in the shadow DOM</span>
  </template>
</div>

然後,我們從 #host 元素獲取 Shadow DOM 的控制代碼並呼叫 setHTMLUnsafe()。輸入已透過轉換函式,因此我們不向方法傳遞 Sanitizer。

js
const shadow = document.querySelector("#host").shadowRoot;
// setHTMLUnsafe() with no sanitizer (no filtering)
shadow.setHTMLUnsafe(trustedHTML);

不使用受信任型別而使用 setHTMLUnsafe()

此示例演示了我們未使用受信任型別的情況,因此我們將傳遞 Sanitizer 引數。

程式碼首先建立一個未受信任的字串,並展示了將 Sanitizer 傳遞給該方法的多種方式。

js
// The potentially malicious string
const untrustedString = "abc <script>alert(1)<" + "/script> def";

// Get the shadow root element
const shadow = document.querySelector("#host").shadowRoot;

// Define custom Sanitizer and use in setHTMLUnsafe()
// This allows only elements: div, p, button, script
const sanitizer1 = new Sanitizer({
  elements: ["div", "p", "button", "script"],
});
shadow.setHTMLUnsafe(untrustedString, { sanitizer: sanitizer1 });

// Define custom SanitizerConfig within setHTMLUnsafe()
// Removes the <script> element but allows other potentially unsafe entities.
shadow.setHTMLUnsafe(untrustedString, {
  sanitizer: { removeElements: ["script"] },
});

setHTMLUnsafe() 即時示例

此示例提供了該方法在使用不同 Sanitizer 呼叫時“即時”演示。程式碼定義了您可以單擊以注入 HTML 字串的按鈕。一個按鈕完全不進行清理就注入 HTML,第二個按鈕使用允許 <script> 元素但不允許其他不安全項的自定義 Sanitizer。原始字串和注入的 HTML 會被記錄下來,以便您可以檢查每種情況下的結果。

注意: 由於我們想展示 Sanitizer 引數的用法,因此以下程式碼注入的是字串而不是受信任型別。您不應在生產程式碼中這樣做。

HTML

HTML 定義了兩個 <button> 元素,分別用於在不使用 Sanitizer 和使用自定義 Sanitizer 的情況下注入 HTML;另一個按鈕用於重置示例;還有一個 <div> 包含宣告式 Shadow DOM。

html
<button id="buttonNoSanitizer" type="button">None</button>
<button id="buttonAllowScript" type="button">allowScript</button>
<button id="reload" type="button">Reload</button>

<div id="host">
  <template shadowrootmode="open">
    <span>I am in the shadow DOM </span>
  </template>
</div>

JavaScript

首先,我們定義了重新載入按鈕的處理程式。

js
const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());

然後,我們定義要注入 Shadow DOM 的輸入字串,該字串在所有情況下都相同。它包含 <script> 元素和 onclick 處理程式,這兩者都被認為是 XSS 不安全的。我們還獲取了變數 shadow,即 Shadow DOM 的控制代碼。

js
// Define unsafe string of HTML
const unsanitizedString = `
  <div>
    <p>Paragraph to inject into shadow DOM. <button onclick="alert('You clicked the button!')">Click me</button></p>
    <script src="path/to/a/module.js" type="module"><script>
  </div>
`;

const shadow = document.querySelector("#host").shadowRoot;

接下來,我們定義不傳遞 Sanitizer 的按鈕的點選處理程式,該按鈕使用 setHTMLUnsafe() 設定 Shadow DOM。由於沒有 Sanitizer,我們期望注入的 HTML 與輸入字串匹配。

js
const buttonNoSanitizer = document.querySelector("#buttonNoSanitizer");
buttonNoSanitizer.addEventListener("click", () => {
  // Set the content of the element with no sanitizer
  shadow.setHTMLUnsafe(unsanitizedString);

  // Log HTML before sanitization and after being injected
  logElement.textContent = "No sanitizer\n\n";
  log(`\nunsanitized: ${unsanitizedString}`);
  log(`\nsanitized: ${shadow.innerHTML}`);
});

下一個點選處理程式使用自定義 Sanitizer 設定目標 HTML,該 Sanitizer 只允許 <div><p><script> 元素。

js
const allowScriptButton = document.querySelector("#buttonAllowScript");
allowScriptButton.addEventListener("click", () => {
  // Set the content of the element using a custom sanitizer
  const sanitizer1 = new Sanitizer({
    elements: ["div", "p", "script"],
  });
  shadow.setHTMLUnsafe(unsanitizedString, { sanitizer: sanitizer1 });

  // Log HTML before sanitization and after being injected
  logElement.textContent = "Sanitizer: {elements: ['div', 'p', 'script']}\n";
  log(`\nunsanitized: ${unsanitizedString}`);
  log(`\nsanitized: ${shadow.innerHTML}`);
});

結果

點選“無”和“允許指令碼”按鈕,分別檢視無 Sanitizer 和自定義 Sanitizer 的效果。

當您點選“無”按鈕時,您應該會看到輸入和輸出匹配,因為沒有應用 Sanitizer。當您點選“允許指令碼”按鈕時,<script> 元素仍然存在,但 <button> 元素被移除。透過這種方法,您可以建立安全的 HTML,但不必強制執行。

規範

規範
HTML
# dom-shadowroot-sethtmlunsafe

瀏覽器相容性

另見