透過 Sanitizer API 進行 Safe DOM 操控

新的 Sanitizer API 旨在建構強大的處理器,以便將任意字串安全插入網頁。

Jack J
Jack J

應用程式會持續處理不受信任的字串,但安全地將內容轉譯為 HTML 文件並不容易。如果未做好充分的謹慎,很容易就不小心造成惡意攻擊者可能會利用跨網站指令碼攻擊 (XSS) 的機會。

為降低風險,新的 Sanitizer API 提案旨在建構強大的處理工具,以便將任意字串安全插入網頁。本文將介紹這個 API 並說明其用途。

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

提報使用者輸入內容

將使用者輸入內容、查詢字串、Cookie 內容等項目插入 DOM 時,字串必須正確逸出。特別注意必須透過 .innerHTML 處理 DOM 操作,其中未逸出的字串是 XSS 的典型來源。

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

如果您在上方輸入字串中逸出 HTML 特殊字元,或是使用 .textContent 展開該字串,系統將不會執行 alert(0)。不過,由於使用者新增的 <em> 也會以字串的形式展開,因此這個方法無法用來在 HTML 中保留文字裝飾。

建議你不要使用「逸出」功能,而是要執行「清除」作業。

清除使用者輸入內容

逸出和清理功能之間的差異

「逸出」是指將特殊 HTML 字元替換成 HTML 實體

「清理」是指移除 HTML 字串中會造成語意有害的部分 (例如執行指令碼)。

範例

在上一個範例中,<img onerror> 會導致錯誤處理常式執行,但如果 onerror 處理常式已移除,您可以在 DOM 中安全地展開該處理常式,同時保留 <em>

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

為了正確清理作業,您必須將輸入字串剖析為 HTML、省略視為有害的標記和屬性,並保留無害的標記。

建議的 Sanitizer API 規格旨在以標準 API 的形式提供這類處理內容,以供瀏覽器使用。

Sanitizer API

以下使用 Sanitizer API

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

不過,{ sanitizer: new Sanitizer() } 是預設引數。如下所示。

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

值得注意的是,Element 已定義 setHTML()。做為 Element 的方法,要剖析的結構定義為自我解釋 (在此案例中為 <div>),剖析會在內部執行一次,且結果會直接展開至 DOM。

為了以字串的形式取得掃毒結果,您可以從 setHTML() 結果中使用 .innerHTML

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

透過設定自訂

根據預設,Sanitizer API 會移除會觸發指令碼執行的字串。不過,您也可以透過設定物件,在掃毒程序中加入自己的自訂設定。

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

以下選項會指定淨化結果應如何處理指定元素。

allowElements:消毒器應保留的元素名稱。

blockElements:清理器應移除的元素名稱,同時保留子項。

dropElements:消毒器應移除的元素名稱及其子項。

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

您也可以透過下列選項,控制是否允許或拒絕指定屬性:

  • allowAttributes
  • dropAttributes

allowAttributesdropAttributes 屬性必須使用屬性比對清單:這些物件的鍵為屬性名稱,值則是目標元素清單或 * 萬用字元。

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements 是允許或拒絕自訂元素的選項。即使允許,元素和屬性的其他設定仍然適用。

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

API 介面

與 DomPurify 比較

DOMPurify 是知名的程式庫,提供掃毒功能。Sanitizer API 和 DOMPurify 的主要差異在於 DOMPurify 會以字串形式傳回清理結果,您必須透過 .innerHTML 寫入 DOM 元素。

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify 可做為瀏覽器未導入 Sanitizer API 時的備用方案。

DOMPurify 實作有幾項缺點。如果傳回字串,則 DOMPurify 和 .innerHTML 會剖析輸入字串兩次。這種雙重剖析作業會浪費處理時間,但也可能導致由於第二次剖析的結果與初次剖析結果不同,造成一些有趣的安全漏洞。

HTML 也必須經過剖析 context。例如,<td><table> 中具有意義,但在 <div> 中則不行。由於 DOMPurify.sanitize() 只接受字串做為引數,因此必須猜測剖析結構定義。

Sanitizer API 有助於改善 DOMPurify 方法,還能讓使用者不必重複剖析及釐清剖析內容。

API 狀態和瀏覽器支援

正在標準化流程中討論 Sanitizer API,Chrome 也處於導入程序。

步驟 狀態
1. 建立說明 完成
2. 建立規格草稿 完成
3. 收集意見回饋並改進設計 完成
4. Chrome 來源試用 完成
5. 推出 預計於 M105 出貨

Mozilla:認為這項提案值得設計原型,且已積極導入

WebKit:在 WebKit 郵寄清單中查看回應。

如何啟用 Sanitizer API

瀏覽器支援

  • x
  • x
  • x

資料來源

透過 about://flags 或 CLI 選項啟用

Chrome

Chrome 正在實作 Sanitizer API。在 Chrome 93 以上版本中,您可以啟用 about://flags/#enable-experimental-web-platform-features 標記來試用這種行為。在舊版的 Chrome Canary 和開發人員版中,您可以透過 --enable-blink-features=SanitizerAPI 啟用並立即試用。請參閱使用旗標執行 Chrome 的操作說明

Firefox

Firefox 同時以實驗性功能實作 Sanitizer API。如要啟用這項功能,請在 about:config 中將 dom.security.sanitizer.enabled 旗標設為 true

功能偵測

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

意見回饋:

如果您已經試用過這個 API,並提供寶貴意見,我們非常樂意聽聽您的想法。分享您對 Sanitizer API GitHub 問題有什麼想法,並與對這個 API 感興趣的規格作者和同事討論。

如果在 Chrome 實作過程中發現任何錯誤或非預期的行為,請回報錯誤。選取 Blink>SecurityFeature>SanitizerAPI 元件並分享詳細資料,協助實作者追蹤問題。

操作示範

如要查看 Sanitizer API 的運作情形,請參閱 Mike West 設計的 Sanitizer API Playground

參考資料


相片來源:Towfiqu barbhuiya 顯示在 Unsplash 上。