使用 HTML Sanitizer API

HTML Sanitizer API 提供了允许开发人员将不可信的 HTML 安全地注入到 ElementShadowRootDocument 中的方法。如果需要,该 API 还为开发人员提供了进一步限制或扩展允许的 HTML 实体的灵活性。

默认安全清理

该 API 最常见的用例是将用户提供的字符串安全地注入到 Element 中。除非要注入的字符串需要包含不安全的 HTML 实体,否则你可以使用 Element.setHTML() 作为 Element.innerHTML 的替代品。

例如,以下代码将删除输入字符串中所有不安全的 XSS 元素和属性(在此例中为 <script> 元素),以及 HTML 规范不允许作为目标元素子元素的任何元素

js
const untrustedString = "abc <script>alert(1)<" + "/script> def";
const someElement = document.getElementById("target");

// someElement.innerHTML = untrustedString;
someElement.setHTML(untrustedString);

console.log(someElement.innerHTML); // abc def

其他 XSS 安全的方法,ShadowRoot.setHTML()Document.parseHTML(),以相同的方式使用。

安全方法进一步限制了允许的实体

你可以通过在所有清理方法的第二个参数中传入 Sanitizer 来指定要允许或删除的 HTML 实体。

例如,如果你知道下面的“someElement”上下文中只期望出现 <p><a> 元素,你可能会创建一个只允许这些元素的清理器配置

js
sanitizerOne = Sanitizer({ elements: ["p", "a"] });
sanitizerOne.allowAttribute("href");
someElement.setHTML(untrustedString, { sanitizer: sanitizerOne });

请注意,使用安全方法时,不安全的 HTML 实体总是会被删除。当与安全方法一起使用时,一个宽松的清理器配置将允许与默认配置相同或更少的实体。

允许不安全的清理

有时你可能希望注入需要包含潜在不安全元素或属性的输入。在这种情况下,你可以使用 API 的 XSS 不安全方法之一:Element.setHTMLUnsafe()ShadowRoot.setHTMLUnsafe()Document.parseHTMLUnsafe()

一种常见的方法是从默认清理器开始,它只允许安全元素,然后只允许输入中我们期望出现的那些不安全实体。

例如,在以下清理器中,所有安全元素都被允许,我们还允许在 button 元素上使用不安全的 onclick 处理程序(仅限)。

js
const untrustedString = '<button onclick="alert(1)">Button text</button>';
const someElement = document.getElementById("target");

sanitizerOne = Sanitizer(); // Default sanitizer
sanitizerOne.allowElement({ name: "button", attributes: ["onclick"] });
someElement.setHTMLUnsafe(untrustedString, { sanitizer: sanitizerOne });

使用此代码,alert(1) 将被允许,并且存在属性可能被用于恶意目的的潜在问题。但是我们知道所有其他 XSS 不安全的 HTML 实体都已被删除,所以我们只需要担心这一个案例,并可以采取其他缓解措施。

不安全方法将使用你提供的任何清理器配置(或不使用),因此你需要比使用安全方法时更小心。

允许配置

你可以通过指定在使用清理器时要允许注入的 HTML 元素和属性集来构建“允许”清理器配置。这种形式的配置易于理解,并且在你确切知道目标上下文中应允许哪些 HTML 实体时很有用。

例如,以下配置“允许” <p><div> 元素以及 citeonclick 属性。它还用其内容替换 <b> 元素(这是一种“允许”的形式,因为元素内容没有被删除)。

js
const sanitizer = Sanitizer({
  elements: ["p", "div"],
  attributes: ["cite", "onclick"],
  replaceWithChildrenElements: ["b"],
});

允许元素

允许的元素可以使用传递给 Sanitizer() 构造函数(或直接传递给清理方法)的 SanitizerConfig 实例的 elements 属性来指定。

使用该属性最简单的方法是指定一个元素名称数组

js
const sanitizer = Sanitizer({
  elements: ["div", "span"],
});

但是你也可以使用定义其 namenamespace 的对象来指定每个允许的元素,如下所示(如果可能,Sanitizer 将自动推断命名空间)。

js
const sanitizer = Sanitizer({
  elements: [
    {
      name: "div",
      namespace: "http://www.w3.org/1999/xhtml",
    },
    {
      name: "span",
      namespace: "http://www.w3.org/1999/xhtml",
    },
  ],
});

你可以使用 Sanitizer 的 API 将元素添加到 Sanitizer 中。这里我们将相同的元素添加到空的清理器中

js
const sanitizer = Sanitizer({});
sanitizer.allowElement("div");
sanitizer.allowElement({
  name: "span",
  namespace: "http://www.w3.org/1999/xhtml",
});

允许全局属性

要全局允许属性,即在 HTML 规范允许的任何元素上,可以使用 SanitizerConfigattributes 属性。

使用 attributes 属性最简单的方法是指定一个属性名称数组

js
const sanitizer = Sanitizer({
  attributes: ["cite", "onclick"],
});

你也可以像元素一样,用 namenamespace 属性指定每个属性

js
const sanitizer = Sanitizer({
  attributes: [
    {
      name: "cite",
      namespace: null,
    },
    {
      name: "onclick",
      namespace: null,
    },
  ],
});

你还可以使用 SanitizerallowAttribute() 方法将每个允许的属性添加到 Sanitizer

js
const sanitizer = Sanitizer({});
sanitizer.allowAttribute("cite");
sanitizer.allowAttribute("onclick");

允许/删除特定元素上的属性

你还可以允许或删除特定元素上的属性。请注意,这是“允许配置”的一部分,因为在这种情况下,你仍然允许注入该元素。

要在元素上允许属性,你可以将该元素指定为一个具有 nameattributes 属性的对象。attributes 属性包含该元素上允许的属性数组。

下面我们展示了一个清理器,其中 <div><a><span> 元素被允许,并且 <a> 元素还允许 hrefrelhreflangtype 属性。

js
const sanitizer = Sanitizer({
  elements: [
    "div",
    { name: "a", attributes: ["href", "rel", "hreflang", "type"] },
    "span",
  ],
});

同样,我们可以使用带有 removeAttributes 属性的元素对象来指定不允许在元素上使用的属性。例如,以下清理器将从所有 <a> 元素中删除 type 属性。

js
const sanitizer = Sanitizer({
  elements: ["div", { name: "a", removeAttributes: ["type"] }],
});

在这两种情况下,你还可以将每个属性指定为具有 namenamespace 属性的对象。你还可以使用传递给 Sanitizer.allowElement() 的相同元素对象来设置属性属性。

但是请注意,你不能在一个调用中同时指定元素 attributesremoveAttributes。尝试这样做将引发异常。

替换子元素

你可以指定一个元素数组,用其内部内容替换。这最常用于从元素中去除样式。

例如,以下代码使用 SanitizerConfigreplaceWithChildrenElements 属性来指定应替换 <b> 元素

js
const replaceBoldSanitizer = Sanitizer({
  replaceWithChildrenElements: ["b"],
});

targetElement.setHTML("This <b>highlighting</b> isn't needed", {
  sanitizer: replaceBoldSanitizer,
});

// Log the result
console.log(targetElement.innerHTML); // This highlighting isn't needed

与元素和属性一样,你也可以指定带命名空间的替换元素,或者使用 Sanitizer.replaceElementWithChildren() 方法

js
const sanitizer = Sanitizer({});
sanitizer.replaceElementWithChildren("b");
sanitizer.replaceElementWithChildren({
  name: "i",
  namespace: "http://www.w3.org/1999/xhtml",
});

删除配置

你可以通过指定在使用清理器时要从输入中删除的 HTML 元素和属性集来构建“删除”清理器配置。配置允许所有其他元素和属性,尽管如果你在安全清理方法中使用该配置,它们可能会被删除。

注意:清理器配置可以包含允许列表或删除列表,但不能同时包含两者。

例如,以下配置删除了 <script><div><span> 元素以及 onclick 属性。

js
const sanitizer = Sanitizer({
  removeElements: ["script", "div", "span"],
  removeAttributes: ["onclick"],
});

当你想调整现有配置时,指定要删除的元素会更有用。例如,考虑我们正在使用(安全的)默认清理器,但也要确保以下情况:

js
const sanitizer = Sanitizer();
sanitizer.removeElement("div");

const sanitizer = Sanitizer({
  removeElements: ["script", "div", "span"],
  removeAttributes: ["onclick"],
});

移除元素

SanitizerConfig 实例的 removeElements 属性可用于删除元素。

使用该属性最简单的方法是指定一个元素名称数组

js
const sanitizer = Sanitizer({
  removeElements: ["div", "span"],
});

允许元素一样,你也可以使用定义其 namenamespace 的对象来指定要删除的每个元素。你还可以使用 Sanitizer API 配置删除的元素,如下所示

js
const sanitizer = Sanitizer({});
sanitizer.removeElement("div");
sanitizer.removeElement({
  name: "span",
  namespace: "http://www.w3.org/1999/xhtml",
});

移除属性

SanitizerConfigremoveElements 属性可用于指定要全局移除的属性。

使用该属性最简单的方法是指定一个元素名称数组

js
const sanitizer = Sanitizer({
  removeAttributes: ["onclick", "lang"],
});

你也可以使用定义其 namenamespace 的对象来指定每个元素,并且还可以使用 Sanitizer.removeAttribute() 来添加要从所有元素中删除的属性。

js
const sanitizer = Sanitizer({});
sanitizer.removeAttribute("onclick");
sanitizer.removeAttribute("lang");

注释和数据属性

SanitizerConfig 也可用于指定是否应从注入的内容中过滤注释和 data- 属性,分别使用 commentsdataAttributes 布尔属性。

要同时允许注释和数据属性,你可以使用这样的配置

js
const sanitizer = Sanitizer({
  comments: true,
  dataAttributes: true,
});

你也可以使用 Sanitizer.setComments()Sanitizer.setDataAttributes() 方法,在现有清理器上启用或禁用注释或数据属性

js
const sanitizer = Sanitizer({});
sanitizer.setComments(true);
sanitizer.setDataAttributes(true);

Sanitizer 与 SanitizerConfig

所有清理方法都可以传入一个清理器配置,该配置可以是 SanitizerSanitizerConfig 实例。

Sanitizer 对象是 SanitizerConfig 的包装器,它提供了额外的有用功能

  • 默认构造函数创建一个允许所有 XSS 安全元素和属性的配置,因此它是创建稍微更严格或稍微不那么严格的清理器的一个很好的起点。
  • 当你使用这些方法来允许或删除 HTML 实体时,这些实体会从“相反”的列表中删除。这些标准化使配置更加高效。
  • Sanitizer.removeUnsafe() 方法可用于从现有配置中删除所有 XSS 不安全的实体。
  • 你可以导出配置以准确查看允许和删除的实体。

请注意,如果你可以使用安全的清理方法,那么你可能根本不需要定义清理器配置。

示例

有关其他示例,请参阅 HTML Sanitizer APISanitizer 接口的各个方法。

清理器演示

此示例演示了如何使用 Sanitizer 方法更新清理器。结果是一个演示界面,你可以在其中将元素和属性添加到允许列表和删除列表,并查看当清理器与 Element.setHTML()Element.setHTMLUnsafe() 一起使用时它们的效果。

HTML

首先我们定义按钮来重置默认清理器或一个空的清理器。

html
<div class="button-group">
  <button id="defaultSanitizerBtn">Default Sanitizer</button>
  <button id="emptySanitizerBtn">Empty Sanitizer</button>
</div>

接下来是 <select> 元素,允许用户选择要添加到元素和属性的允许列表和删除列表中的元素。

html
<div class="select-group">
  <label for="allowElementSelect">allowElement:</label>
  <select id="allowElementSelect">
    <option value="">--Choose element--</option>
    <option value="h1">h1</option>
    <option value="div">div</option>
    <option value="span">span</option>
    <option value="script">script</option>
    <option value="p">p</option>
    <option value="button">button</option>
    <option value="img">img</option>
  </select>

  <label for="removeElementSelect">removeElement:</label>
  <select id="removeElementSelect">
    <option value="">--Choose element--</option>
    <option value="h1">h1</option>
    <option value="div">div</option>
    <option value="span">span</option>
    <option value="script">script</option>
    <option value="p">p</option>
    <option value="button">button</option>
    <option value="img">img</option>
  </select>
</div>
<div class="select-group">
  <label for="allowAttributeSelect">allowAttribute:</label>
  <select id="allowAttributeSelect">
    <option value="">--Choose attribute--</option>
    <option value="class">class</option>
    <option value="autocapitalize">autocapitalize</option>
    <option value="hidden">hidden</option>
    <option value="lang">lang</option>
    <option value="title">title</option>
    <option value="onclick">onclick</option>
  </select>
  <label for="removeAttributeSelect">removeAttribute:</label>
  <select id="removeAttributeSelect">
    <option value="">--Choose attribute--</option>
    <option value="class">class</option>
    <option value="autocapitalize">autocapitalize</option>
    <option value="hidden">hidden</option>
    <option value="lang">lang</option>
    <option value="title">title</option>
    <option value="onclick">onclick</option>
  </select>
</div>

然后我们添加按钮来切换允许/删除注释和数据属性。

html
<div class="button-group">
  <button id="toggleCommentsBtn">Toggle comments</button>
  <button id="toggleDataAttributesBtn">Toggle data-attributes</button>
</div>

其余元素显示要解析的字符串(可编辑)以及使用 setHTML()setHMLUnsafe() 分别注入到元素中时这两个字符串的结果

html
<div>
  <p>Original string (Editable)</p>
  <pre contenteditable id="unmodified"></pre>
  <p>setHTML() (HTML as string)</p>
  <pre id="setHTML"></pre>
  <p>setHTMLUnsafe() (HTML as string)</p>
  <pre id="setHTMLUnsafe"></pre>
</div>

JavaScript

代码首先测试是否支持 Sanitizer 接口。然后它定义了一个“不安全 HTML”字符串,其中包含 XSS 安全和 XSS 不安全元素(例如 <script>)的混合。这作为文本插入到第一个文本区域。文本区域是可编辑的,因此用户可以根据需要稍后更改文本。

然后,我们获取 setHTMLsetHTMLUnsafe 文本区域的元素,我们将在其中写入解析后的 HTML,并创建一个空的 Sanitizer 配置。使用新的清理器调用 applySanitizer() 方法,以记录使用安全和不安全清理器清理初始字符串的结果。

js
if ("Sanitizer" in window) {
  // Define unsafe string of HTML
  const initialHTMLString =
    `<div id="mainDiv"><!-- HTML comment -->
    <p data-test="true">This is a paragraph. <button onclick="alert('You clicked the button!')">Click me</button></p>
    <p>Be <b>bold</b> and brave!</p>
    <script>alert(1)<` + "/script></div>";

  // Set unsafe string as a text node of first element
  const unmodifiedElement = document.querySelector("#unmodified");
  unmodifiedElement.innerText = initialHTMLString;
  unsafeHTMLString = unmodifiedElement.innerText;

  const setHTMLElement = document.querySelector("#setHTML");
  const setHTMLUnsafeElement = document.querySelector("#setHTMLUnsafe");
  // Create and apply default sanitizer when we start
  let sanitizer = new Sanitizer({});
  applySanitizer(sanitizer);

下面显示了 applySanitizer() 日志方法。它从第一个文本区域获取“不可信字符串”的初始内容,并使用 Element.setHTML()Element.setHTMLUnsafe() 方法以及传入的 sanitizer 参数将其解析到相应的文本区域中。在每种情况下,注入的 HTML 都从元素中用 innerHTML 读取,然后作为 innerText 写回元素中(以便人类可读)。

然后代码会记录当前的清理器配置,它通过 Sanitizer.get() 获取。

js
function applySanitizer(sanitizer) {
  // Get string to parse into element
  unsafeHTMLString = unmodifiedElement.innerText;

  // Sanitize string using safe method and then display as text
  setHTMLElement.setHTML(unsafeHTMLString, { sanitizer });
  setHTMLElement.innerText = setHTMLElement.innerHTML;

  // Sanitize string using unsafe method and then display as text
  setHTMLUnsafeElement.setHTMLUnsafe(unsafeHTMLString, { sanitizer });
  setHTMLUnsafeElement.innerText = setHTMLUnsafeElement.innerHTML;

  // Display sanitizer configuration
  const sanitizerConfig = sanitizer.get();
  log(JSON.stringify(sanitizerConfig, null, 2));
}

接下来我们获取每个按钮和选择列表的元素。

js
const defaultSanitizerBtn = document.querySelector("#defaultSanitizerBtn");
const emptySanitizerBtn = document.querySelector("#emptySanitizerBtn");
const allowElementSelect = document.querySelector("#allowElementSelect");
const removeElementSelect = document.querySelector("#removeElementSelect");
const allowAttributeSelect = document.querySelector("#allowAttributeSelect");
const removeAttributeSelect = document.querySelector("#removeAttributeSelect");

const toggleCommentsBtn = document.querySelector("#toggleCommentsBtn");
const toggleDataAttributesBtn = document.querySelector(
  "#toggleDataAttributesBtn",
);

前两个按钮的处理程序分别创建默认和空的清理器。我们之前展示的 applySanitizer() 方法用于运行清理器并更新日志。

js
defaultSanitizerBtn.addEventListener("click", () => {
  sanitizer = new Sanitizer();
  applySanitizer(sanitizer);
});

emptySanitizerBtn.addEventListener("click", () => {
  sanitizer = new Sanitizer({});
  applySanitizer(sanitizer);
});

接下来显示选择列表的处理程序。当选择新元素或属性时,这些处理程序会在当前清理器上调用相关的清理器方法。例如,allowElementSelect 的侦听器调用 Sanitizer.allowElement() 以将所选元素添加到允许的元素中。在每种情况下,applySanitizer() 都会使用当前清理器记录结果。

js
allowElementSelect.addEventListener("change", (event) => {
  if (event.target.value !== "") {
    sanitizer.allowElement(event.target.value);
    applySanitizer(sanitizer);
  }
});
removeElementSelect.addEventListener("change", (event) => {
  if (event.target.value !== "") {
    sanitizer.removeElement(event.target.value);
    applySanitizer(sanitizer);
  }
});
allowAttributeSelect.addEventListener("change", (event) => {
  if (event.target.value !== "") {
    sanitizer.allowAttribute(event.target.value);
    applySanitizer(sanitizer);
  }
});
removeAttributeSelect.addEventListener("change", (event) => {
  if (event.target.value !== "") {
    sanitizer.removeAttribute(event.target.value);
    applySanitizer(sanitizer);
  }
});

下面显示了最后两个按钮的处理程序。这些按钮切换 dataAttributesActivecommentsActive 变量的值,然后将这些值用于 Sanitizer.setComments()Sanitizer.setDataAttributes()。请注意,如果注释最初被禁用,第一次按下按钮可能无效!

js
let dataAttributesActive = true;
let commentsActive = true;

toggleCommentsBtn.addEventListener("click", () => {
  commentsActive = !commentsActive;
  sanitizer.setComments(commentsActive);
  applySanitizer(sanitizer);
});

toggleDataAttributesBtn.addEventListener("click", () => {
  dataAttributesActive = !dataAttributesActive;
  sanitizer.setDataAttributes(dataAttributesActive);
  applySanitizer(sanitizer);
});


} else {
  log("The HTML Sanitizer API is NOT supported in this browser.");
  // Provide fallback or alternative behavior
}

结果

结果如下所示。选择顶部按钮分别设置新的默认或空清理器。然后你可以使用选择列表将一些元素和属性添加到相应的清理器允许和删除列表,以及其他按钮来切换注释的开和关。当前清理器配置已记录。顶部文本区域中的文本使用当前清理器配置进行清理,并使用 setHTML()setHTMLUnsafe() 进行解析。

请注意,将元素和属性添加到允许列表会将其从删除列表中删除,反之亦然。另请注意,你可以在清理器中允许元素,这些元素将通过不安全方法注入,但不能通过安全方法注入。