新的 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
處理常式已移除,則在保留 <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
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 正在實作這個 API。
步驟 | 狀態 |
---|---|
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 網站上。