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="">`

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: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 より。