新的 Sanitizer API 旨在建構強大的處理器,以便將任意字串安全插入網頁。
應用程式會持續處理不受信任的字串,但安全地將內容轉譯為 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
allowAttributes
和 dropAttributes
屬性必須使用屬性比對清單:這些物件的鍵為屬性名稱,值則是目標元素清單或 *
萬用字元。
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 出貨 |
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 West 設計的 Sanitizer API Playground:
參考資料
相片來源:Towfiqu barbhuiya 顯示在 Unsplash 上。