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

浏览器兼容性

另见