透過 Sanitizer API 進行 Safe DOM 操控

新的 Sanitizer API 旨在為任意字串建構強大的處理器,以便安全地插入網頁。

Jack J
Jack J

應用程式經常會處理不受信任的字串,但要安全地將該內容算繪為 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;>&quot;/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

您也可以透過下列選項,控管清除器是否允許或拒絕特定屬性:

  • allowAttributes
  • dropAttributes

allowAttributesdropAttributes 屬性會預期屬性比對清單,也就是鍵為屬性名稱的物件,值則是目標元素清單或 * 萬用字元。

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

Mozilla:認為這項提案值得製作原型,並積極實作

WebKit:請參閱 WebKit 郵寄清單中的回覆。

如何啟用 Sanitizer API

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

透過 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 網站上。