透過 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 處理常式已移除,則在保留 <em> 的情況下,有可能在 DOM 中安全地展開這個處理常式。

// 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 正在實作這個 API。

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

Mozilla:認為這個提案值得設計原型,並積極導入

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

如何啟用 Sanitizer API

瀏覽器支援

  • Chrome:不支援。
  • Edge:不支援。
  • Firefox:位於旗幟後方。
  • Safari:不支援。

資料來源

透過 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 網站上。