新しいDOM API、setHTMLを知ろう

新しいDOM API、setHTMLを知ろう

D
dongAuthor
1 min read

Web開発をしていると、動的にHTMLを挿入する必要がある場面がよくあります。このとき最も一般的に使われる方法はinnerHTMLですが、この方法にはクロスサイトスクリプティング(XSS)攻撃に脆弱であるという大きな欠点があります。そのため、多くの開発者はDOMPurifyのような外部ライブラリを使用して、安全にHTMLを挿入してきました。

しかし今後は、Web標準に組み込まれた安全な方法でHTMLを挿入できるようになる見込みです。それが、新しいDOM APIであるsetHTML()の登場です。この記事では、setHTML()とは何か、なぜ必要なのか、そしてどう使うのかを詳しく解説します。

setHTML()とは?

setHTML()は、Elementインターフェースの新しいメソッドで、その名の通りHTML文字列をDOM要素に安全に設定する役割を果たします。このメソッドは単にHTMLを挿入するだけでなく、内蔵のHTML Sanitizer APIを使用して文字列を解析・サニタイズする処理を行います。

setHTML()の主な特徴は以下のとおりです。

  • XSS防止: 入力されたHTML文字列から、潜在的に危険な<script>タグやonerrorのようなインラインイベントハンドラなど、XSS攻撃を引き起こす可能性のある要素を自動的に除去します。
  • 妥当性チェック: 現在の要素のコンテキストに合わない不正な要素を除去します。
  • デフォルトのSanitizer提供: 設定なしでも、デフォルトのSanitizer構成によって安全にHTMLを挿入できます。

MDNドキュメントによれば、setHTML()は許可されていないHTMLエンティティを削除するだけでなく、Sanitizerの設定で許可された場合でも、XSSに安全でない要素や属性は無条件に削除します。これにより、開発者はセキュリティの心配を減らし、ビジネスロジックにより集中できるようになります。

なぜsetHTML()を使うべきなのか?

innerHTMLがあるのに、わざわざsetHTML()を使う必要があるのか疑問に思うかもしれません。ここでは2つのメソッドを比較しながら、setHTML()の利点を見ていきましょう。

innerHTMLとの比較

innerHTMLは手軽に使えますが、最大の問題はセキュリティです。外部から受け取った信頼できない文字列をそのままinnerHTMLに代入すると、攻撃者が仕込んだ悪意のあるスクリプトがそのまま実行される可能性があります。

const userInput = `<img src=x onerror="alert('XSS攻撃!')">`;
const container = document.getElementById('container');

// userInputに含まれるスクリプトが実行され、アラートが表示されます。
container.innerHTML = userInput;

一方、setHTML()はこのような危険を根本的に防ぎます。

const userInput = `<img src=x onerror="alert('XSS攻撃!')">`;
const container = document.getElementById('container');

// onerror属性がSanitizerによって除去され、スクリプトは実行されません。
container.setHTML(userInput);

setHTMLUnsafe()との比較

setHTML()とともに、setHTMLUnsafe()というメソッドもあります。名前に「Unsafe」が付いていることからも分かるように、このメソッドは基本的にHTMLをサニタイズしません。XSSに脆弱な要素を含める必要がある、非常に特殊な場合を除いては、setHTML()の使用が強く推奨されます。setHTMLUnsafe()は信頼できるソースからのHTMLを挿入する場合に限定して使用すべきです。

結論として、setHTML()innerHTMLの利便性を維持しつつ、セキュリティを大幅に向上させた、安全な代替手段と言えるでしょう。

Element: setHTMLUnsafe() method

setHTML()の使い方

setHTML()の基本的な使い方はとても簡単です!

// 基本的な使い方
const unsanitizedString = "これは<b>安全な</b>HTMLです。<script>alert('これは実行されません!')</script>";
const targetElement = document.getElementById("target");

// デフォルトSanitizerを使用してHTMLを挿入
targetElement.setHTML(unsanitizedString);

このコードを実行すると、<script>タグは除去され、「これは安全なHTMLです。」というテキストだけがレンダリングされます。

options.sanitizerでカスタマイズ

setHTML()は第2引数としてoptionsオブジェクトを受け取り、Sanitizerの動作を制御できます。options.sanitizerプロパティを使用すると、許可または除去するタグや属性を自分で指定できます。

const unsanitizedString = "<div>divタグ</div> <p>pタグ</p> <button>buttonタグ</button>";
const targetElement = document.getElementById("target");

// 1. Sanitizerオブジェクトを直接作成して渡す
// div, p, buttonタグのみ許可します。
const customSanitizer = new Sanitizer({
  elements: ["div", "p", "button"],
});
targetElement.setHTML(unsanitizedString, { sanitizer: customSanitizer });

// 2. SanitizerConfigオブジェクトをインラインで渡す
// divとpタグを除去します。
targetElement.setHTML(unsanitizedString, {
  sanitizer: { removeElements: ["div", "p"] },
});

SanitizerConfigを使う場合に、allowElementsremoveElementsを同時に定義するなど、正規化されていない設定を渡すとTypeErrorが発生するので注意が必要です。

ブラウザ対応状況はまだ…

残念ながら、setHTML()はまだ実験的な機能であり、この記事執筆時点では主要なブラウザのほとんどが対応していません。FirefoxのNightly版ではデフォルトで有効化されており、今後他のブラウザにも広がっていくことが期待されます。

したがって、実際のプロダクション環境で使う前には必ずMDNのブラウザ対応表を確認する必要があります。対応が整うまでは、Sanitizer API Polyfillを使って似た機能を実装することができます。

コミュニティの反応

setHTML()の登場は、多くの開発者コミュニティで好意的に受け入れられています。特にHacker Newsのようなフォーラムでは、「25年越しについに出た」と歓迎ムードです。

  • ライブラリ依存の軽減: 軽量ライブラリLitの開発者は、setHTML()が標準化されれば、unsafeHTMLディレクティブの代わりにこれを使って、DOMPurifyのような外部依存なしで安全なHTMLレンダリングができるようになると期待しています。DOMPurifyは強力ですが、lit-htmlのコアより数倍も大きいため、組み込みAPIの登場は大きなメリットです。
  • innerHTMLの安全な代替: 多くの開発者は、setHTML()innerHTMLの安全版、またnpmで広く使われているDOMPurifyの組み込みバージョンとして理解しています。これはWebプラットフォーム全体のセキュリティが一段と進化したことを意味します。

Hacker news

個人的にも本当にうれしいニュースです!

setHTML()は、innerHTMLが抱える慢性的なセキュリティ問題を解決し、Web標準に組み込まれた安全なHTML挿入手段を提供する重要なAPIです。まだ実験段階ではありますが、この機能がすべてのブラウザに正式導入されれば、開発者は外部ライブラリへの依存を減らし、より安全で堅牢なWebアプリケーションを構築できるようになるでしょう。

すぐにプロダクションに導入するのは難しいかもしれませんが、この新しいAPIの存在を認識し、テストしながら将来に備えるのはいかがでしょうか?setHTML()がもたらすより安全なWeb開発環境に期待しましょう。

個人的にこのブログでもHTMLを直接扱うことが多いので、本当にうれしいニュースです!🤣
早く安定化してほしいですね!🥹

References

新しいDOM API、setHTMLを知ろう | devdong