新版 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>
您也可以透過下列選項,控管清除器是否允許或拒絕特定屬性:
allowAttributesdropAttributes
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 也需要 context 才能剖析。舉例來說,<td> 在 <table> 中有意義,但在 <div> 中則沒有。由於 DOMPurify.sanitize() 只會將字串做為引數,因此必須猜測剖析內容。
Sanitizer API 改進了 DOMPurify 方法,可避免重複剖析,並釐清剖析環境。
API 狀態和瀏覽器支援
標準化程序正在討論 Sanitizer API,Chrome 則正在實作這項 API。
WebKit:請參閱 WebKit 郵寄清單中的回覆。
如何啟用 Sanitizer API
Browser Support
透過 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 網站上。