新的 Sanitizer API 旨在為任意字串建構強大的處理器,以便安全地插入網頁。
應用程式經常會處理不受信任的字串,但要安全地將該內容算繪為 HTML 文件的一部分,可能相當棘手。如果沒有充分注意,很容易就會不小心為惡意攻擊者創造跨網站指令碼 (XSS) 攻擊的機會。
為降低這項風險,新的 Sanitizer API 提案旨在建構任意字串的強大處理器,以便安全地插入網頁。本文將介紹這項 API,並說明其用途。
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())
逸出使用者輸入內容
將使用者輸入內容、查詢字串、Cookie 內容等插入 DOM 時,字串必須經過適當逸出。請特別留意透過 .innerHTML 進行的 DOM 操控,因為未逸出的字串是 XSS 的常見來源。
const user_input = `<em>hello world</em><img src="" onerro>r=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="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`
如要正確清除,必須將輸入字串剖析為 HTML,省略有害的標記和屬性,並保留無害的標記和屬性。
建議的 Sanitizer API 規格旨在提供這類處理作業,做為瀏覽器的標準 API。
Sanitizer API
Sanitizer API 的使用方式如下:
const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div
不過,{ sanitizer: new Sanitizer() } 是預設引數。因此可以像下方一樣。
$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>"/div
請注意,setHTML() 是在 Element 上定義的。做為 Element 的方法,要剖析的內容不言而喻 (本例中為 <div>),剖析會在內部執行一次,結果會直接擴展到 DOM 中。
如要以字串形式取得清除結果,可以使用 setHTML() 結果中的 .innerHTML。
const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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" <]})> })
//< >divhe<ll><o bw>orld/b/div
$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div
您也可以透過下列選項,控管清除器是否允許或拒絕特定屬性:
allowAttributesdropAttributes
allowAttributes 和 dropAttributes 屬性會預期屬性比對清單,也就是鍵為屬性名稱的物件,值則是目標元素清單或 * 萬用字元。
const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`
$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div
$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<
$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div
$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/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 >})
//< divcustom-e><lemh>ello/custom-elem/div
API 介面
與 DomPurify 比較
DOMPurify 是提供清除功能的名聲顯赫程式庫。Sanitizer API 與 DOMPurify 的主要差異在於,DOMPurify 會以字串形式傳回清除結果,您必須透過 .innerHTML 將該字串寫入 DOM 元素。
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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。
| 步驟 | 狀態 |
|---|---|
| 1. 建立說明 | 完成 |
| 2. 建立規格草稿 | 完成 |
| 3. 收集意見回饋並逐步調整設計 | 完成 |
| 4. Chrome 來源試用 | 完成 |
| 5. 啟動 | Intent to Ship on M105 |
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 網站上。