新しい 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>
)、解析は内部で 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="">`
Sanitizer API がブラウザに実装されていない場合は、DOMPurify を代替として使用できます。
DOMPurify の実装にはいくつかの欠点があります。文字列が返された場合、入力文字列は DOMPurify と .innerHTML
によって 2 回解析されます。この二重解析は処理時間を浪費しますが、2 回目の解析の結果が 1 回目と異なる場合、興味深い脆弱性につながる可能性があります。
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 を有効にする方法
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: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 をご覧ください。
参照
- HTML Sanitizer API の仕様
- WICG/sanitizer-api リポジトリ
- Sanitizer API に関するよくある質問
- MDN の HTML Sanitizer API リファレンス ドキュメント
写真提供: Towfiqu barbhuiya、Unsplash より。