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

Element: setHTML() メソッド

概要

  • setHTML() はHTML文字列を安全にDOMへ挿入するためのメソッド
  • XSS対策 として、危険な要素や属性を自動で除去
  • SanitizerやSanitizerConfig によるカスタマイズ可能
  • Element.innerHTMLsetHTMLUnsafe() より安全
  • Trusted Types API による検証は行われない

setHTML() メソッドの概要

  • setHTML(input, options) は、HTML文字列を安全にパースし、要素内に挿入するDOMメソッド
  • input :サニタイズ対象となるHTML文字列
  • options(省略可能) :サニタイズ方法を指定するオブジェクト
    • sanitizer :Sanitizerインスタンス、SanitizerConfigオブジェクト、または"default"文字列を指定可能
  • 戻り値 :なし(undefined)
  • 例外 :TypeError
    • 不正なSanitizerConfig (allowedとremoved両方指定など)、"default"以外の文字列、型不一致時に発生

setHTML() の動作詳細

  • XSS対策 として、HTML文字列から危険な要素や属性を自動で削除
  • 要素の文脈依存性 を考慮し、無効な要素(例:<col><table>外にある場合)も除去
  • Sanitizer設定 で許可されていても、危険な要素・属性は必ず削除
  • options.sanitizer 未指定時は デフォルトSanitizer が適用
    • デフォルトは「安全」と判断された要素・属性のみ許可
  • カスタムSanitizer/SanitizerConfig で許可・除去要素や属性、コメントなどを柔軟に指定可能
  • Sanitizer.removeUnsafe() が暗黙的に呼ばれるため、常に安全性を確保

使用推奨シーン

  • Element.innerHTML の代替として、信頼できないHTML文字列の挿入
  • Element.setHTMLUnsafe() の代替(安全性を重視する場合)
  • XSSリスク低減 を目的としたDOM操作

具体的な使い方

  • デフォルトSanitizerの場合
    • 例:
      const unsanitizedString = "abc <script>alert(1)</script> def";
      const target = document.getElementById("target");
      target.setHTML(unsanitizedString); // <script>タグは除去される
      
  • カスタムSanitizerの利用
    • 例:
      const sanitizer1 = new Sanitizer({ elements: ["div", "p", "button", "script"] });
      target.setHTML(unsanitizedString, { sanitizer: sanitizer1 }); // script要素も除去
      
  • SanitizerConfigを直接指定
    • 例:
      target.setHTML(unsanitizedString, { sanitizer: { removeElements: ["div", "p", "button", "script"] } });
      

ライブデモの流れ

  • HTML構成
    • 複数のボタン(Default, allowScript, Reload)
    • 挿入対象の<div>とログ表示用<pre>
  • JavaScript処理
    • sanitizer適用ボタン でsetHTMLを実行し、サニタイズ前後の内容をログに出力
    • defaultSanitizer :危険な要素・属性はすべて除去
    • allowScriptSanitizer :script要素を許可してもsetHTMLでは除去される
    • Reloadボタン :ページリロードで初期化
    • Sanitizer未対応ブラウザ :代替処理案内

結果・注意事項

  • どのSanitizer設定でも<script>要素やonclick属性は除去
  • setHTML()は常に安全性を最優先
  • Trusted Types API による追加の検証は行われない点に留意

参考リンク

  • HTML Sanitizer API仕様 :HTML Sanitizer API# dom-element-sethtml
  • 関連API
    • Element.setHTMLUnsafe()
    • ShadowRoot.setHTML(), ShadowRoot.setHTMLUnsafe()
    • Document.parseHTML(), Document.parseHTMLUnsafe()
  • 公式ドキュメント参照推奨

Hackerたちの意見

つまり、これは基本的にinnerHTMLの安全版ってこと?

そうだね、もうちょっと適切に言うと、これは内蔵のDOMPurifyって感じかな(dompurifyは、HTMLを注入する前にサニタイズするためによく使われるnpmパッケージ)。

HTMLを生成してインジェクトしてるのに、なんで「安全な」バージョンが必要なのか、ちょっと混乱してる。

今週、Firefox Nightly(だけ)でこれをデフォルトで有効にしたよ。

Litがベースラインに達したら、これを使うのがめっちゃ楽しみ!lit-htmlのテンプレートは、テンプレート文字列が改ざんできないからXSS対策がされてるけど、unsafeHTML()みたいなユーティリティもあって、信頼できない文字列をHTMLとして扱えるんだ。でも、今のところそれは…危険なんだよね。Element.setHTML()を使えば、safeHTML()ディレクティブを作って、開発者がサニタイズオプションを指定できるようにできるよ。

25年もこれなしでやってきたから、やっと見られて嬉しい!DOM APIの明らかな欠けてる部分だと思ってたし、なんでこんなに時間がかかったのか未だにわからない。でも、やっと実現したことが嬉しいし、これを実現するために頑張ってくれた人たちには感謝してる。

そうだね、リプライが来たね。

サニタイザーの設定で許可されていないHTMLエンティティを削除し、さらにXSSに対して安全でない要素や属性も削除します — サニタイザーの設定で許可されているかどうかに関わらず。強調は俺の。 この設計選択が理解できない。scriptタグを明示的に許可しているのに、なんでそれが削除されるの?もしこのメソッドがsetXSSSafeSubsetOfHTMLだったら理解できるけど、setHTMLでオーバーライド不可能なフィルターがあるのは変な感じ。

XSSに対して安全でないサニタイザーを使いたいなら、setHTMLUnsafeを使わなきゃ。

安全なデフォルトを目指してるんだろうね…ドキュメントをちゃんと読まない人や、動的に生成されたHTMLの出所を気にしない人が「setHTML()」を使うだろうから。その一方で、「setHTMLUnsafe()」もあるし、もちろんお馴染みの.innerHTMLもね。

これは主に使いやすさを考えた追加だから、危険な足元をより使いやすくするのはあまり意味がないと思う。危険なことをするためにinnerHTMLなどを使うことはできるしね。

それって、setHTMLを再度呼び出せるコードを許可することで、権限をエスカレートさせる引き金になっちゃうんじゃない?

スクリプトタグは、設定したサニタイズをバイパスしてsetHTMLUnsafeを呼び出せるよね。私は、unsafeな設定でsetHTMLを呼び出すとランタイムエラーになるようにすべきだと思うけど、JavaScriptはエラーを出すよりも暗黙の再解釈に傾くからね。

安全なバージョンを使いやすくしないとダメだよ。C++のメモリバグの多くは、標準委員会が未定義動作のバージョンを安全なものよりも3文字短くするせいなんだ。 (今もやってるし!最近C++23に追加された別の例を見つけたよ)

setHTMLにオーバーライド不可能なフィルターがあるのは変な感じ。 そんなことはないよ。安全な動作が重要だっていう経験が何十年もあるから。 > このデザインの選択が理解できない。もしscriptタグを明示的に許可してるなら、なんでそれが削除されるの?壊れてない状況が無限に少ないからで、それに到達するためには努力が必要だってことだよ。innerHTMLはまだあるし、setHTMLUnsafeはデフォルトでフィルタリングが全くない(innerHTMLが行うスクリプトの無効化すらない)。

コンテンツインジェクションの脆弱性にずっと悩まされてきた身としては、やっとこれが見られて嬉しい!他の面倒な解決策、例えばCSPが何年も前からあるのに、今になってこれが出てくるのはちょっとクレイジーだね。

もしかしたら、JavaScriptドキュメントの最初に「use strict」を使う以上の何かが必要な時期かもしれないね。サニタイズや他のスクリプト設定のオプションを定義するための設定オブジェクトがあれば便利かも。結局、ほとんどの場合、後方互換性を確保する必要があるし、これがうまくいくかも。私は仕様の専門家じゃないけど、ただのアイデアだよ。Reactは「use client/server」を使ってるから、これがもっと中心的で明示的になると思う。

それじゃあ、.setHTML("...")はHTMLを設定しないってこと?

それは十分に理にかなってると思う。99.99%の確率で、実際のスクリプトの中にいる時は、コードを実行したいなら、自分で実行するだけだよね。コード満載のスクリプトタグを作って、ランダムなDOM要素にそのタグを突っ込むなんてしない。だから、デフォルトではスクリプトタグを尊重せず、「unsafe」なメソッドが明示的に名前付けされて、変なことをしてるって教えてくれるんだ。

.innerHTML = "..." も同じだよ。

「XSS-unsafe」ってどこかでちゃんと定義されてるの? たぶん「JSインタープリターへのアクセスがある」ってことだと思うけど、この文脈での仮定はちょっと危険だよね。

入力から何をサニタイズするかは「sanitizer」オプションパラメータで調整できるみたい。ただ、デフォルトのサニタイザーはドキュメントページにリンクされてる仕様で定義されてるよ。[1] https://wicg.github.io/sanitizer-api/#dom-element-sethtml [2] https://wicg.github.io/sanitizer-api/#sanitize

これは嬉しいニュースだね。やっと! innerHTMLより安全で予測可能な代替手段ができた!

それは本当に良いニュースだね。LLMがXSS脆弱なコードを喜んで生成するのを見て、しばらくは状況が悪化するだろうなと思ったよ。実際、claude-codeにテンプレートライブラリを使わせるのがすごく難しかったし、XSS脆弱性がある手書きのテンプレートにデフォルトで戻りたがるのを避けるのも大変だった。選択肢を一緒に考えた後でもね。エスケープとサニタイズの違いも扱うのが難しいし、異なるアプローチやサニタイザーを混ぜるのは危険なこともある。setHTML()のような安全なバックストップがあれば、間違う方法を狭める素晴らしい追加になるね。