新版 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>
值得注意的是,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
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="">`
如果瀏覽器未實作 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. 啟動 | Intent to Ship on 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 網站上提供。