掲示板やコメント欄のユーザー名に <script>alert(1)</script> を入れられたらどうなるでしょうか。 エスケープを忘れると、そのページを開いた全ユーザーのブラウザで攻撃者のJavaScriptが実行されます。 これがXSS(クロスサイトスクリプティング)です。この記事では、HTMLエスケープが必要な理由と、 エスケープすべき5文字・コンテキスト別の扱い・Reactの自動エスケープとの関係を解説します。
なぜHTMLエスケープが必要か — XSS攻撃の例
HTMLエスケープは、ユーザー入力やDBから取り出した文字列をHTMLに埋め込む前に、 特殊文字を「実体参照(エンティティ)」に変換する処理です。これを忘れると、 入力文字列がHTMLの一部として解釈されてしまい、タグやスクリプトを注入されます。
たとえば、ユーザー名をそのままHTMLに埋め込んでいるサイトに、次のような名前で登録されたとします。
<!-- ユーザー名をそのまま出力(危険) -->
<div>こんにちは、<script>alert('XSS')</script> さん</div>
<!-- ブラウザはscriptタグを解釈して実行してしまう -->このとき、攻撃者は単にアラートを出すだけでなく、document.cookie を外部サーバーに送信してセッションを乗っ取ったり、 フォームにキーロガーを仕込んだりできます。エスケープを通せば、同じ文字列も次のように無害なテキストになります。
<!-- エスケープ後(安全) -->
<div>こんにちは、<script>alert('XSS')</script> さん</div>
<!-- 画面には文字列としてそのまま表示される -->エスケープすべき5文字
HTMLエスケープの基本は、以下の5文字を実体参照に置き換えることです。 OWASP(Open Worldwide Application Security Project、Webセキュリティの国際コミュニティ)の推奨もこの5文字です。
| 文字 | 実体参照 | 数値参照 | エスケープが必要な理由 |
|---|---|---|---|
| < | < | < | タグの開始文字。これを許すとscriptなど任意のタグ注入が可能 |
| > | > | > | タグの終了文字。対称性のためエスケープ(必須ではないが推奨) |
| & | & | & | 実体参照の開始文字。最初に変換しないと二重エスケープの元になる |
| " | " | " | 属性値の区切り。これを許すと属性を閉じて新しい属性を注入される |
| \' | ' | ' | 属性値をシングルクォートで囲む場合の区切り |
注意点として、& を最初に変換するのが鉄則です。< を先に < に変換してから & を & に変換すると、 すでに変換済みの < がさらに &lt; に変わってしまい、画面に < と表示されてしまいます。 実装済みのHTMLエスケープツールは変換順を正しく実装しているので、検算にも使えます。
コンテキスト別の扱い
エスケープは「どこに値を出力するか」によって必要な処理が変わります。 同じユーザー入力でも、HTMLテキスト・属性値・URL・JavaScript文字列では別の対策が必要です。
| 出力先 | 必要な処理 | 例 |
|---|---|---|
| HTMLテキスト | 5文字のHTMLエスケープ | <div>{escape(text)}</div> |
| HTML属性値 | HTMLエスケープ+属性値を必ず " または \' で囲む | <input value="{escape(v)}"> |
| URL(href・src) | URLエンコード+HTMLエスケープ。javascript: スキームを弾く | <a href="{escape(encodeURI(url))}"> |
| JavaScript文字列 | JavaScriptエスケープ(\\u003c など) | <script>const x = "{jsEscape(v)}"</script> |
| CSS(style属性) | 原則埋め込まない。必要なら事前にホワイトリスト検証 | 動的CSSは避ける |
特に注意したいのが URL です。<a href="javascript:alert(1)">のような疑似プロトコルはHTMLエスケープでは防げません。href に動的な値を入れるなら、スキームがhttp: / https: / mailto:などであることを検証してから出力するのが鉄則です。
Reactの自動エスケープと独自実装の違い
モダンなフレームワーク(React・Vue・Angularなど)では、テンプレート内に変数を埋め込むと 自動的にHTMLエスケープが行われます。Reactの場合、<div>{userInput}</div>と書けば、userInput に <script>が含まれていても、エスケープされたテキストとして表示されます。
ただし、次のAPIは自動エスケープの対象外です。開発者が明示的に生のHTMLを差し込める抜け道なので、使う際は特に注意が必要です。
dangerouslySetInnerHTML(React)/v-html(Vue) — 渡したHTML文字列がそのままDOMに挿入されるhref="javascript:..."— URLはエスケープされないため疑似プロトコル攻撃が通る- 属性名が動的に決まる場合(
<div {...props}>にonclickを注入されるなど) - SVGや
iframeの srcdoc 属性
Markdownレンダラーや外部HTMLの埋め込みで dangerouslySetInnerHTMLを使うときは、DOMPurifyなどのサニタイザを必ず通します。自動エスケープに頼れない場面だけを正しく識別することが、実務でのXSS対策の核心です。
サニタイズ vs エスケープ — どちらを選ぶか
HTMLエスケープとよく混同されるのが「サニタイズ」です。似た目的を持ちますが、アプローチが違います。
| 項目 | エスケープ | サニタイズ |
|---|---|---|
| 方針 | 特殊文字をすべて実体参照に置換 | HTMLとしてパースし危険な要素だけ除去 |
| 結果 | タグが文字列として表示される | 安全なタグ(<b>・<a>等)は残る |
| 用途 | ユーザー名・コメント・検索語など「タグを使わせたくない」場面 | ブログ本文・リッチテキスト投稿など「一部タグを許したい」場面 |
| 代表的な実装 | 5文字の置換(数行で書ける) | DOMPurify・sanitize-html(ライブラリ必須) |
迷ったらまずエスケープで済まないか検討するのが安全です。 サニタイズは設定項目が多く、ホワイトリストの漏れが新たな攻撃経路になりえます。 「どうしてもユーザーにHTMLを書かせたい」場面でだけサニタイザを使い、それ以外はエスケープで統一するのがシンプルで堅実です。
まとめ
- XSSはエスケープ忘れで他人のブラウザで任意のJavaScriptを実行される脆弱性
- エスケープすべきは
< > & " \'の5文字。& を最初に変換するのが鉄則 - 出力先(HTMLテキスト・属性・URL・JavaScript)ごとに必要な処理が違う。どれか1つで済むとは限らない
- Reactの自動エスケープは強力だが、
dangerouslySetInnerHTMLやhref="javascript:..."は対象外 - エスケープはテキスト化、サニタイズはタグの取捨選択。まずエスケープで済むならエスケープを選ぶ
- DBには生データを保存し、出力する瞬間にコンテキストに合わせてエスケープする