世界を動かす技術を、日本語で。

さようなら InnerHTML、こんにちは SetHTML: Firefox 148における強化されたXSS保護

概要

  • Cross-site scripting (XSS) は依然としてウェブで最も一般的な脆弱性の一つ
  • Sanitizer API は未信頼なHTMLを安全にDOMへ挿入するための標準API
  • Firefox 148 が最初にSanitizer APIを実装し、今後他ブラウザも対応予定
  • setHTML() メソッドで安全なHTML挿入が容易に
  • Trusted Types との併用で更なるセキュリティ向上が可能

XSS(クロスサイトスクリプティング)とその課題

  • XSS脆弱性 は、ユーザー生成コンテンツ経由で攻撃者が任意のHTMLやJavaScriptを注入できる状態
  • 攻撃者による ユーザー操作の監視・データ窃取 のリスク
  • CWE-79 で10年以上トップ3にランクインする深刻な問題
  • Content-Security-Policy (CSP) の標準化でMozillaがリード
    • サイトごとに読み込めるリソースを制限し、XSS対策の強力な手段
  • CSPの導入障壁 :既存サイトへの大幅なアーキテクチャ変更や専門家による継続的なレビューが必要

Sanitizer APIの登場とメリット

  • Sanitizer API は、悪意あるHTMLを無害化する標準的な方法を提供
  • setHTML()メソッド でHTML挿入時に自動でサニタイズ
  • 例:
    • document.body.setHTML('<h1>Hello my name is <img src="x" onclick="alert(\'XSS\')">');
    • <h1>Hello my name is</h1>だけが残り、危険な<img>onclick属性は除去
  • innerHTML の置き換えで安全性向上、コード変更も最小限
  • カスタム設定 も可能:用途に合わせて許可する要素や属性を指定

導入と実験方法

  • Sanitizer API Playground で事前に動作確認が可能
  • Trusted Types との連携でHTML解析・挿入の一元管理
    • setHTML()導入後、Trusted Typesの強制も容易
    • setHTML()のみ許可し、他の危険なHTML挿入方法をブロック可能

Firefox 148の対応と今後の展望

  • Firefox 148 はSanitizer APIおよびTrusted Typesの両方をサポート
  • 開発者は 専任セキュリティチームや大幅な実装変更なし でXSS対策が可能
  • 今後他ブラウザも標準対応 見込み、ウェブ全体のセキュリティ向上に貢献

著者紹介

  • Tom Schuster :複数のセキュリティ関連記事を執筆
  • Frederik Braun :Mozilla Firefoxのセキュリティ開発者
    • Sanitizer APIやSubresource Integrityなどの標準仕様に貢献
    • 趣味は小説読書やヨーロッパのサイクリング
  • Christoph Kerschbaumer :Security Engineeringマネージャー

参考リンク・クレジット

  • イラスト:Desi Ratna(Website)、Made by Made(Person)、Andy Horvath(Hacker)
  • Frederik Braun公式サイト:https://frederikbraun.de

Hackerたちの意見

こういうのはいつも緊張するんだよね。ユーザーからの入力を安全に処理できるメソッドと、脆弱性を引き起こす可能性があるメソッドが混ざっちゃうから、どれがどれか名前だけじゃ全然わからないし。理想的には、そういう危険な関数は名前から明確に危険だってわかるように設計するべきなんだけど、後からそれをやるのは簡単じゃないんだよね。それに、HTMLを「サニタイズ」するものにはちょっと懐疑的なんだ。穴がある歴史が長いし、そもそも「サニタイズ」って何を意味するのか、何が「安全」とされるのかもすぐにはわからないから。

要するに、innerHTMLとsetHTMLを混ぜないってこと。innerHTMLの使用を全部排除して、古い機能が必要なときは新しいsetHTMLUnsafeを使うって感じ。

デフォルト設定の「安全」についてのリンクもあるよ。 https://wicg.github.io/sanitizer-api/#built-in-safe-default-... でも、同意するけど、僕のデフォルトのアプローチは、信頼できないコンテンツがある場合はinnerTextだけを使うことが多いかな。だから、彼らのデモがこれだとしたら:container.SetHTML(Hello, {name}); 僕のはこうなる:let greetingHeader = container.CreateElement("h1"); greetingHeader.innerText = Hello, {name};

realSetSafeHTML()

「安全」という概念があいまいなのはその通りだけど、ここでの目標は特にXSSに対して安全であることなんだ。スクリプトが実行される可能性のある要素やプロパティは削除される。この機能はユーザーエージェントに組み込まれていて、安全でない要素がDOMに追加されるのを防ぐから、文字列から文字列へのサニタイザーよりも正しく処理しやすいはず。要素が現在DOMに追加されているかどうかを判断するのは、「このHTML文字列にスクリプトタグが含まれているかどうか」を判断するよりも基本的に簡単だからね。

ブラウザネイティブのサニタイザーAPIには、ライブラリアプローチにはない利点があるんだ。それは、ブラウザがレンダリングに使うのと同じHTMLパーサーを使っていること。DOMPurifyみたいなライブラリは、別のコンテキストでパースしてから再シリアライズするけど、歴史的にその往復がバイパスの大半を生んできたんだよね。サニタイザーとレンダラーが同じパーサーを共有していると、ミューテーションXSS攻撃は隠れる場所がなくなる。

ちょっとしたサニタイズでもないよりはマシ?ブラウザに任せてるなら、もうかなりヤバいことになってるよ。

参考までに、ページを「Content-Security-Policy: require-trusted-types-for 'script'」で提供すると、サニタイズしないメソッドに通常の文字列を渡すのをブロックできるよ。

理想を言えば、ウェブ開発者としてどこかに古いAPI(innerHTMLみたいな)を禁止するグローバルプロパティを設定できるべきだけど、その場合は「あなたのウェブサイトはXより古いブラウザでは動かない」という大きな注意事項がつくね。でも、もしかしたらそれに関するウェブ標準がすでにあるかも。古いブラウザの場合はバックアップコンテンツを用意する必要があるし。

名前からはどっちがどっちか全然わからないよね。setHTML と setHTMLUnsafe があるけど、これ以上わかりやすくするのは難しいかも。

こういうのが出てきて嬉しいけど、ブラウザのサポートが広がるまでには時間がかかりそうだね。 https://caniuse.com/mdn-api_element_sethtml

確かに、どんなブラウザAPIでも、数年後には使えるようになるかもしれないし、最近のバージョンに満足しているなら数ヶ月で使えるようになるかもね。その間はポリフィルがあるかもしれないし。

これはいいね。ネットワークアクセスのすべての側面がきちんと制御されているのが一番のポイント。セキュリティが信頼できるコードの連鎖から、ホスト上の信頼できるセキュリティ設定の連鎖に移行したから、既存の安全なデフォルトがちゃんと機能してる。

だから、例に挙げたように、ユーザー名にまだインジェクトできるってことだよね。スクリプト実行というバグクラスを防ぐのはいいけど、ドキュメントを正しく読んでるなら、ページに任意のマークアップ(CSSルールも含む)を許可してしまうんだよね。これを使ったら、プロフィールページを開く人にはPayPalが新鮮に見えるかもしれない。誰がこんなのを望むんだろう?

だから、例に挙げたように、ユーザー名にまだインジェクトできるってことだよね。setHTMLが入力をサニタイズするのに、どうやって?HTMLタグを一切許可したくないなら、もう設定できるみたいじゃない?

誰がこんなのを望むんだろう?思いつくのは、フォーラムの機能を求めるケースかな。ユーザーにマークダウンで書けるようにしたいかもしれない。これなら、マークダウンから生成されたHTMLをさらに制限して、h1のような許可された要素だけにすることで、予期しないマークダウンのエスケープハッチを試みる人に備えられるよ。

でも、これだとページに任意のマークアップ(CSSルールも含む)を許可してしまうんだよね。もしそれが本当なら、最近のCSSでできることを考えると、まだセキュリティリスクがあるように思えるね。

setHTML()のデフォルト設定が特定のユースケースに対して厳しすぎる(または緩すぎる)場合、開発者はどのHTML要素や属性を保持するか、または削除するかを定義するカスタム設定を提供できる。

これが正しく読めてるなら、.setHTML("Hello", new Sanitizer({}))はすべての要素を取り除くことになるね。それ自体はそんなに難しくないし。さらに、これは防御の深さを考えたものだよ。バックエンドは、どんな標準でもユーザー名をサニタイズする必要があるし(任意のUnicode入力をユーザー名として受け入れるべきシステムはあまりないから)、バックエンドは出力するものをHTMLエスケープするべきだよ。 [1]: https://www.rfc-editor.org/rfc/rfc2119

setHTMLinnerHTMLの代わりとして考えられてるよ。君が説明してるユースケースでは、そもそもinnerHTMLは必要なかったはず。innerTexttextContentが欲しいところだね。

こんなの誰が欲しがるの?ある程度の柔軟性を持たせたいけど、限度はあるってことだよね。例えば、フォーラムの投稿で許可したいけど、これはダメっていうのを設定したい時とか。使い道は想像しやすいよ。

マークアップを使いたくないなら innerText もあるよ。もっと詳しく言うと、document.createTextNode の後に whatever.appendChild って感じ。

だから、ユーザー名にインジェクトしたり...とかできるよ。ウェブの楽しさを全部奪っちゃうの?初期の Facebook の頃の人たちの名前が大好きだったんだ、無害な楽しみだったよ。もしフロントエンドのコードのインジェクションでバックエンドが落ちるなら、バックエンドがダメなんだよ、直せ。

これ、いろいろと危険が潜んでる気がする。特にサニタイザーAPIとやり取りする場合、特に「remove」サニタイザーAPIを使うときはね。悪くはないけど、何もないよりはマシだけど、ユーザーにHTMLをドキュメントに追加させないように「setText」を使うことを真剣に考えた方がいいよ。

アロウリストベースのサニタイザーを使えば、自分で自分の足を撃つ可能性は確実に減るけど、setHTMLを使う限り、少なくともXSSを引き起こすことはないよね。

「この要素のテキスト/HTMLを設定する」の開発は、いつ完了とみなせるんだろう?

ブラウザがデータとコードを分けるバリアントを実装する時が来るかもね。それが見出しを読んだときに期待してたことだよ:setHtml(code, data, data, ...)、まるでパラメータ化されたSQLみたいに。prepare("select rowid from %s where time < %n", tablename, mynumber) もしHTMLがマークアップ言語以外のものであれば、彼らが考えたこの新しいメソッドはeval(code, options)と呼ばれるはず。

おお、それは嬉しいね。Mozilla、いい仕事してるよ。もしポリシーを使って特定のページでinnerHTMLをオフにできれば、もっと良くなるけど、plain-JavaScriptアプリケーションにとっては確実に正しい方向への一歩だね。

innerHTML が好ましい状況ってあるのかな?パフォーマンスが良いかもしれないし、XSS に対してオープンじゃないものを構築するなら理論上は良いかも(ただし、こういうことに関しては人間はいつもミスをするっていう前提はあるけど)。

古い挙動を setHTMLUnsafe って名付けたのが決定打だった。開発者がオプトインしなきゃいけないセキュリティ機能は機能しない。危険な道を危険に感じさせることが大事なんだよ。

まあ、SetHTML って名前、もしくは .set_html() は .inner_html() よりも客観的に意味があるよね。 .inner_html = .set_inner_html() って感じで。小さなことだけど…本当に。いつか誰かが JavaScript のゴチャゴチャを整理してほしいな。多分それは起こらないだろうけど、JavaScript には変なことがたくさんあるし…ここで話してるのは攻撃からの保護についてだから、より良い API デザインじゃないのはわかるけど、本当に - API は理想的には公開された瞬間から最高であるべきだよね。

ちょっと細かいことを言うと、これは JavaScript に公開されている DOM API のことだよ。DOM API は常に、そして今も、API を作ったことがない人たちによって書かれたように感じる。