使用 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() 進行解析。

請注意,將元素和屬性新增到允許列表會將其從刪除列表中刪除,反之亦然。另請注意,你可以在清理器中允許元素,這些元素將透過不安全方法注入,但不能透過安全方法注入。