Sanitizer API による安全な 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 に挿入する場合は、文字列を適切にエスケープする必要があります。エスケープされていない文字列が XSS の一般的な原因となる .innerHTML を介した DOM 操作には特に注意が必要です。

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 ハンドラが削除された場合、<em> をそのままにして DOM で安全に展開できます。

// 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>)であり、解析は内部で 1 回だけ行われ、結果は 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

allowAttributes プロパティと dropAttributes プロパティは、属性一致リスト(キーが属性名で、値がターゲット要素のリストまたは * ワイルドカードであるオブジェクト)を想定しています。

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

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

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 を有効にする方法

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 と 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