Chrome 146の安定版がリリースされ、Scroll-triggered animations、Scoped custom element registries、そして Sanitizer API の3つが主なハイライトとして公式に紹介されています。
その中でも、フロントエンド実務に引きつけて考えたとき、特に気になったのが Sanitizer API です。
新機能の話は、ともすると「へえ、追加されたんだ」で終わりがちです。
でも今回は、そういう温度感で流すには少し惜しいと感じました。
なぜならこれ、単なるAPI追加の話ではなくて、ずっと雑に扱われがちだった innerHTML まわりを「ちゃんと設計する」方向へ寄せてくれる話だからです。
この記事は、Chrome 146の新機能紹介をする記事というより、
「Sanitizer APIをどう実務の文脈で見るか」 を整理する記事です。
先に3行で
-
innerHTMLは便利ですが、使いどころを間違えると事故の入口になります - Sanitizer API は デフォルトのままでも安全で、
setHTML()に置き換えるだけでXSSリスクを大幅に下げられます - ただし、これを使えばXSS対策が完結する、という話ではありません
Sanitizer APIは万能薬ではありません。
危険な実装を減らすための"強い部品"として捉えるのが、正しい使い方への第一歩です。
少し歴史的な話をしておくと
Sanitizer API は今回が初めての取り組みではありません。
実は一度、以前のバージョンのSanitizer APIがChromeに実装されましたが、仕様の問題からいったん撤回されています。
今回Chrome 146で実装されたのは、その反省を踏まえた仕様を刷新した新バージョンです。
また、FirefoxはChrome 146より先にあたるFirefox 148でこの新バージョンのSanitizer APIを実装しており、2026年2月の段階でFirefoxが世界初実装となっています。
Chrome 146は、そのFirefoxに続く形で安定版への実装を果たしました。
「標準化されたSanitizer APIがChromeにも来た」というのが正確な表現です。
Firefoxが先行し、Chromeが続いた——という経緯を知っていると、この機能への理解が深まります。
なぜ今この話が大事なのか
フロントエンドでは、ただの文字列ではなく「HTMLとして一部リッチに表示したい」場面が案外多いです。
たとえば、こんなケースがあります。
- コメント欄で改行やリンクを活かしたい
- CMSから来た本文をそのまま表示したい
- Markdown変換後のHTMLをプレビューしたい
- 管理画面で装飾つきの入力内容を確認したい
このとき、実装者の頭に真っ先に浮かびやすいのが innerHTML です。
書くのも読むのも速いですし、最初はうまく動いて見えます。
でも、怖いのはいつも「最初はうまく動く」実装です。
innerHTML が危ないのは、雑に正しそうに見えるから
たとえば次のコードは、とても自然に見えます。
const preview = document.getElementById("preview");
preview.innerHTML = userInput;
何が厄介かというと、見た目上は正しく動いてしまうことです。
レビューでも「あ、プレビュー表示ね」で流されやすい。
でも本質的には、
信頼できない入力を
HTMLとして解釈させている
という構造を作っています。
そしてこの危なさが、コードの短さに埋もれて見えにくい。
そこが本当に厄介なところです。
たとえばこんな入力が来たとき、どうなるでしょうか。
const userInput = `<h1>こんにちは</h1><img src="x" onclick="alert('XSS')">`;
preview.innerHTML = userInput;
<img> タグの onclick が実行可能な状態でDOMに入ります。
ページを表示するだけで、攻撃者の仕込んだスクリプトが動く状態になります。
textContent で済むなら、まずそれを使うべきです。
問題になるのは、「HTMLとして一部だけ許可したい」ときです。
Sanitizer APIの核心は「safe by default」
Sanitizer APIの一番大事な特徴は、設定ゼロでも安全なことです。
次のコードを見てください。
// Before
preview.innerHTML = userInput;
// After
preview.setHTML(userInput);
setHTML() はデフォルトで危険な要素や属性を自動的に除去します。
先ほどの例でいうと、
document.body.setHTML(`<h1>こんにちは</h1><img src="x" onclick="alert('XSS')">`);
このコードが生成するHTMLは次のようになります。
<h1>こんにちは</h1>
<img> タグごと、危険な onclick 属性ごと落とされます。
これが「safe by default」という設計思想です。
innerHTML は「書いた通りに入れる」動作です。
setHTML() は「安全なものだけ入れる」動作です。
この発想の転換がSanitizer APIの本質です。
カスタム設定で「何を通すか」を定義する
デフォルトの動作が厳しすぎる、あるいは要件に合わないときは、許可リストを自分で定義できます。
// 許可するタグと属性を明示的に定義する
const config = {
allowElements: [
"p", "strong", "em", "a",
"code", "pre", "ul", "ol", "li", "br"
],
allowAttributes: {
a: ["href", "title", "target", "rel"]
}
};
preview.setHTML(userInput, { sanitizer: config });
ここで注目してほしいのは、コードから読み取れる情報量です。
innerHTML の場合は「何を信頼しているのか」が見えません。
カスタム設定の setHTML() の場合は「どのHTMLを通したいのか」が明示されています。
これ、コードレビューで見える景色がまるで違います。
Before / After で比較する
Before(リスクが埋もれている実装)
function renderComment(html) {
commentArea.innerHTML = html;
}
After(デフォルトで安全な実装)
function renderComment(html) {
commentArea.setHTML(html);
}
After(許可ルールを明示した実装)
const commentSanitizeConfig = {
allowElements: [
"p", "strong", "em", "a",
"code", "pre", "ul", "ol", "li", "br"
],
allowAttributes: {
a: ["href", "title", "target", "rel"]
}
};
function renderComment(html) {
commentArea.setHTML(html, { sanitizer: commentSanitizeConfig });
}
どの段階のAfterを選ぶかは要件次第ですが、
少なくともデフォルト版のAfterへの移行はほぼコストゼロでできます。
"便利な新機能"ではなく、"レビューしやすい実装"になったのが大きい
実務では安全性そのものと同じくらい、
安全にしようとしている意図がコードから読めることが重要です。
| 観点 | 雑な innerHTML
|
setHTML() を使う実装 |
|---|---|---|
| 実装の速さ | 🟢 速い | 🟢 ほぼ変わらない |
| 安全性(デフォルト) | 🔴 崩れやすい | 🟢 safe by default |
| レビューのしやすさ | 🔴 意図が見えにくい | 🟢 許可ルールが見えやすい |
| 拡張時の事故リスク | 🔴 起きやすい | 🟢 ルール変更として扱いやすい |
| チーム共有 | 🔴 属人化しやすい | 🟢 方針化しやすい |
特に「実装の速さ」がほぼ変わらないのが今回の大きなポイントです。
innerHTML を setHTML() に置き換えるだけなら、コストはほぼゼロです。
ここで勘違いしたくないこと
ここまで読むと「setHTML() に替えればもう安心では?」と思いたくなります。
でも、そこはかなり慎重に見たほうがいいです。
これだけで終わらない理由
- どの要素・属性を許可するかは、結局設計の問題
-
hrefを許すなら、値にjavascript:が入り込む可能性も考える必要がある - 外部リンクなら
rel="noopener noreferrer"の方針も要る - サーバー側の検証・CSP・入力バリデーションが不要になるわけではない
「setHTML() を使った」こと自体に価値があるのではありません。
何を許可し、何を落とすかを設計できているかに価値があります。
ここを曖昧にしたまま導入すると、
単に innerHTML の置き換え先が増えただけで終わります。
さらに強くするなら Trusted Types との組み合わせ
Sanitizer API単体でも十分に強力ですが、Trusted Typesと組み合わせることで、
HTML挿入を一元管理し、将来的なXSSの回帰を防ぎやすくなります。
Firefox 148はSanitizer APIとTrusted Typesの両方をサポートしており、
この2つを組み合わせることが「最大限のXSS対策」として公式に推奨されています。
📌 ありがちな失敗パターン(読み飽き防止に展開してみてください)
「全部禁止」で逃げる
「危ないから全部テキスト変換」にすると、リッチな表現ができなくなってUXが落ちます。
安全だけど、プロダクトとして死んでいる実装です。
「全部通す」で逃げる
「面倒だから許可リストを広くしておく」にすると、安全性が崩れます。
実装は楽ですが、事故ったときに取り返しがつきません。
正解は「境界を定義する」
- 何を表現として許可したいのか
- 何をアプリとして許可したくないのか
- その境界をコードでどう表現するのか
Sanitizer APIの価値は、この境界を"雰囲気"ではなく実装として書きやすい点にあります。
結局、どこで使うべきなのか
使いどころが明確な場面
- ユーザー投稿をプレビュー表示したい
- Markdown変換後のHTMLを表示したい
- CMS由来のリッチテキストを表示したい
- 「全部テキスト逃げ」だとUXが明らかに落ちる
逆に、慎重に考えたい場面
- 許可ルールがチームで決まっていない
- 信頼できるHTMLと信頼できないHTMLが混在している
- リッチテキスト機能が今後どんどん増える予定
- バックエンド側の入力バリデーション設計が未成熟
まとめると
Chrome 146でSanitizer APIが実装されました。
FirefoxがFirefox 148で先行し、Chromeがそれに続いた形で、この標準化されたAPIが主要ブラウザに揃ってきました。
innerHTML を便利だから使う、から
「安全なデフォルトを持つ setHTML() で扱う」へ。
移行コストはほぼゼロ。
それでいて、得られる安心感は大きい。
Chrome 146の中でも、個人的にはそこがいちばん現場で効いてくるポイントだと感じています。