透過 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>

值得注意的是,setHTML() 是在 Element 上定義。由於 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="">`

如果瀏覽器未實作 Sanitizer API,DOMPurify 可做為備用方案。

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

HTML 也需要上下文才能進行剖析。舉例來說,<td> 適用於 <table>,但不適用於 <div>。由於 DOMPurify.sanitize() 只會將字串做為引數,因此必須猜測剖析內容。

Sanitizer API 能改善 DOMPurify 方法,用來避免重複剖析及釐清剖析環境。

API 狀態和瀏覽器支援

Sanitizer API 目前正在標準化程序中討論中,Chrome 也正在實作中。

步驟 狀態
1. 建立說明 完成
2. 建立規格草稿 完成
3. 收集意見回饋並重複設計 完成
4. Chrome 來源試用版 完成
5. 啟動 打算在 M105 出貨

Mozilla:考慮這個提案的原型,並積極導入

WebKit:查看 WebKit 郵寄清單的回應。

如何啟用 Sanitizer API

透過 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 WestSanitizer API Playground

參考資料


相片來源:Towfiqu barbhuiyaUnsplash 網站上提供。