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 |
浏览器兼容性
加载中…