Sanitizer API による安全な DOM 操作

新しい Sanitizer API は、任意の文字列をページに安全に挿入できる堅牢なプロセッサを構築することを目的としています。

Jack J
Jack J

アプリケーションは常に信頼できない文字列を扱いますが、そのようなコンテンツを 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>)。解析は内部で 1 回実行され、結果は 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="">`

DOMPurify は、Sanitizer API がブラウザに実装されていない場合のフォールバックとして機能します。

DOMPurify の実装にはいくつかの欠点があります。文字列が返されると、入力文字列は DOMPurify と .innerHTML によって 2 回解析されます。この二重解析は処理時間を浪費しますが、2 回目の解析の結果が最初の解析と異なる場合に生じる興味深い脆弱性につながる可能性もあります。

HTML の解析には、コンテキストも必要です。たとえば、<td><table> では意味がありますが、<div> では意味がありません。DOMPurify.sanitize() は文字列のみを引数として受け取るため、解析コンテキストを推測する必要がありました。

Sanitizer API は DOMPurify のアプローチを改善し、二重解析の必要性をなくし、解析のコンテキストを明確にするように設計されています。

API のステータスとブラウザ サポート

Sanitizer API は標準化プロセスで検討されており、Chrome では実装中です。

ステップ ステータス
1. 説明を作成 完了
2. 仕様のドラフトを作成 完了
3.フィードバックを収集して設計を繰り返す 完了
4. Chrome オリジン トライアル 完了
5. リリース M105 の出荷予定

Mozilla: この提案にはプロトタイピングの価値があると考え、積極的に導入しています

WebKit: WebKit メーリング リストの回答をご覧ください。

Sanitizer API を有効にする方法

対応ブラウザ

  • x
  • x
  • x

ソース

about://flags または CLI オプションを使用して有効にする

Chrome

Chrome では Sanitizer API の実装を進めています。Chrome 93 以降では、about://flags/#enable-experimental-web-platform-features フラグを有効にすることでこの動作を試すことができます。以前のバージョンの Chrome Canary および Dev チャンネルでは、--enable-blink-features=SanitizerAPI から有効にしてすぐにお試しいただけます。フラグを指定して Chrome を実行する手順をご確認ください。

Firefox

Firefox も試験運用版の機能として Sanitizer API を実装しています。有効にするには、about:configdom.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 barbhuiyaUnsplash