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 文字列から削除することを意味します。

上記の例では、<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 によって 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 を有効にする方法

対応ブラウザ

  • Chrome: サポートされていません。 <ph type="x-smartling-placeholder">
  • Edge: サポートされていません。 <ph type="x-smartling-placeholder">
  • Firefox: 旗の裏側。
  • Safari: サポートされていません。 <ph type="x-smartling-placeholder">

ソース

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