ShadowRoot: setHTMLUnsafe() 方法
警告:此方法將其輸入解析為 HTML,並將結果寫入 DOM。此類 API 被稱為 注入槽,如果輸入最初來自攻擊者,則可能成為 跨站點指令碼 (XSS) 攻擊的載體。
您可以透過始終傳遞 TrustedHTML 物件而不是字串並 強制執行可信型別 來緩解此風險。有關更多資訊,請參閱 安全注意事項。
注意: 在支援 ShadowRoot.setHTML() 的瀏覽器中,幾乎總是應該使用該方法而不是本方法,因為它始終會移除 XSS 不安全的 HTML 實體。
ShadowRoot 介面的 setHTMLUnsafe() 方法可用於將 HTML 輸入解析為 DocumentFragment,可以選擇性地過濾掉不需要的元素和屬性,然後用它來替換 Shadow DOM 中現有的樹。
語法
setHTMLUnsafe(input)
setHTMLUnsafe(input, options)
引數
input-
定義要解析的 HTML 的
TrustedHTML或字串例項。 options可選-
一個包含以下可選引數的 options 物件
sanitizer可選-
定義允許或移除輸入內容的元素以及屬性的
Sanitizer或SanitizerConfig物件。這也可以是值為"default"的字串,該值應用具有預設(XSS 安全)配置的Sanitizer。如果未指定,則不使用 Sanitizer。請注意,如果配置需要重用,通常
Sanitizer的效率會比SanitizerConfig高。
返回值
無 (undefined)。
異常
TypeError-
如果出現以下情況,將丟擲此錯誤
- 當 Trusted Types 透過 CSP 強制執行且未定義預設策略時,
input會作為字串傳遞。 options.sanitizer傳遞了一個- 不是
Sanitizer、SanitizerConfig或字串的值。 - 非規範化的
SanitizerConfig(包含“允許”和“移除”配置設定)。 - 不具有值
"default"的字串。
- 不是
- 當 Trusted Types 透過 CSP 強制執行且未定義預設策略時,
描述
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 安全預設 Sanitizer 的 setHTMLUnsafe()。
何時應使用 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 在此情況下的效果。
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 的透明替代品。
if (typeof trustedTypes === "undefined")
trustedTypes = { createPolicy: (n, rules) => rules };
接下來,我們建立一個 TrustedTypePolicy,它定義了一個 createHTML(),用於將輸入字串轉換為 TrustedHTML 例項。通常,createHTML() 的實現使用像 DOMPurify 這樣的庫來清理輸入,如下所示
const policy = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
然後,我們使用此 policy 物件從潛在不安全的輸入字串建立 TrustedHTML 物件。
// 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() 以程式設計方式建立,但在此示例中,我們將以宣告方式建立根。
<div id="host">
<template shadowrootmode="open">
<span>A span element in the shadow DOM</span>
</template>
</div>
然後,我們從 #host 元素獲取 Shadow DOM 的控制代碼並呼叫 setHTMLUnsafe()。輸入已透過轉換函式,因此我們不向方法傳遞 Sanitizer。
const shadow = document.querySelector("#host").shadowRoot;
// setHTMLUnsafe() with no sanitizer (no filtering)
shadow.setHTMLUnsafe(trustedHTML);
不使用受信任型別而使用 setHTMLUnsafe()
此示例演示了我們未使用受信任型別的情況,因此我們將傳遞 Sanitizer 引數。
程式碼首先建立一個未受信任的字串,並展示了將 Sanitizer 傳遞給該方法的多種方式。
// 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。
<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
首先,我們定義了重新載入按鈕的處理程式。
const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());
然後,我們定義要注入 Shadow DOM 的輸入字串,該字串在所有情況下都相同。它包含 <script> 元素和 onclick 處理程式,這兩者都被認為是 XSS 不安全的。我們還獲取了變數 shadow,即 Shadow DOM 的控制代碼。
// 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 與輸入字串匹配。
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> 元素。
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 |
瀏覽器相容性
載入中…