ぱんだツールズぱんだツールズ

セキュリティ

HTMLエスケープとXSS対策 — 「<」「>」「&」を安全に扱う

約6分

掲示板やコメント欄のユーザー名に <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>こんにちは、&lt;script&gt;alert('XSS')&lt;/script&gt; さん</div>

<!-- 画面には文字列としてそのまま表示される -->

エスケープすべき5文字

HTMLエスケープの基本は、以下の5文字を実体参照に置き換えることです。 OWASP(Open Worldwide Application Security Project、Webセキュリティの国際コミュニティ)の推奨もこの5文字です。

文字実体参照数値参照エスケープが必要な理由
<&lt;&#60;タグの開始文字。これを許すとscriptなど任意のタグ注入が可能
>&gt;&#62;タグの終了文字。対称性のためエスケープ(必須ではないが推奨)
&&amp;&#38;実体参照の開始文字。最初に変換しないと二重エスケープの元になる
"&quot;&#34;属性値の区切り。これを許すと属性を閉じて新しい属性を注入される
\'&#39;&#39;属性値をシングルクォートで囲む場合の区切り

注意点として、& を最初に変換するのが鉄則です。< を先に &lt; に変換してから & を &amp; に変換すると、 すでに変換済みの &lt; がさらに &amp;lt; に変わってしまい、画面に &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の自動エスケープは強力だが、dangerouslySetInnerHTMLhref="javascript:..." は対象外
  • エスケープはテキスト化、サニタイズはタグの取捨選択。まずエスケープで済むならエスケープを選ぶ
  • DBには生データを保存し、出力する瞬間にコンテキストに合わせてエスケープする

よくある質問

なぜシングルクォート(')もエスケープする必要があるのですか?

HTML属性値を二重引用符(")ではなく単一引用符(')で囲むコードが存在するためです。たとえば <code>&lt;input value='ユーザー入力'&gt;</code> のような書き方では、ユーザー入力にシングルクォートが含まれていると属性を途中で閉じてしまい、新しい属性(onclickなど)を注入されるリスクがあります。属性値がどちらのクォートで囲まれていても安全になるよう、5文字(&lt; &gt; &amp; " ')をまとめてエスケープするのが安全側の実装です。

URLパラメータもHTMLエスケープが必要ですか?

URLパラメータには「URLエンコード(パーセントエンコーディング)」を使います。HTMLエスケープとは別物です。ただし、そのURLを最終的にHTMLの <code>&lt;a href="..."&gt;</code> に埋め込む場合は、URLエンコード後にHTMLエスケープも行うのが原則です。たとえば検索語をリンクにする場合、まず encodeURIComponent でURLエンコードし、その結果文字列をHTMLエスケープしてから href 属性に出力します。どちらか片方だけでは攻撃経路が残ります。

Reactを使っていれば自動でエスケープされるから安全ですか?

ほぼ安全ですが、例外があります。React は JSX 内の文字列(<code>&lt;div&gt;{userInput}&lt;/div&gt;</code>)を自動でエスケープしますが、<code>dangerouslySetInnerHTML</code>・<code>href="javascript:..."</code>・SVGの一部属性など、開発者が明示的に生のHTMLや危険なURLを差し込めるAPIは自動エスケープの対象外です。Markdownレンダリングや外部HTMLの埋め込みを行う場合は、DOMPurifyなどのサニタイザを別途通す必要があります。

DBに保存するときにHTMLエスケープすべきですか?

いいえ、DB保存時はエスケープしないのが原則です。エスケープは「どこに出力するか」によって必要な処理が変わるため、保存時ではなく出力時(HTML出力時・JSON出力時・CSV出力時など)にそのコンテキストに合わせて行います。保存時にエスケープするとAPIでJSONを返すときに <code>&amp;amp;</code> のように二重エスケープされる原因になります。DBにはユーザー入力をそのまま保存し、表示する瞬間に適切なエスケープを行いましょう。

エスケープとサニタイズは何が違いますか?

エスケープは「特殊文字を実体参照に変換してテキストとして無害化する」処理です。<code>&lt;script&gt;</code> を <code>&amp;lt;script&amp;gt;</code> に変換し、そのまま画面に表示します。サニタイズは「HTMLとしてパースしたうえで危険な要素・属性だけを取り除く」処理です。ブログの投稿欄のように、ユーザーに &lt;b&gt; や &lt;a&gt; などの一部タグを許可したい場面で使います。DOMPurifyなどのライブラリを使い、ホワイトリスト方式で安全な要素だけ残します。まず全エスケープで済むならエスケープが簡単で確実です。

JavaScriptの文字列に埋め込むときもHTMLエスケープでいいですか?

いいえ、<code>&lt;script&gt;</code>タグ内のJavaScript文字列に値を埋め込む場合はJavaScriptエスケープ(\u202e などのUnicodeエスケープ・バックスラッシュによるクォート回避)が必要です。HTMLエスケープだけではJavaScriptの構文として &lt; や &gt; がそのまま残るため、攻撃者が <code>&lt;/script&gt;</code> を入力値に含めるとスクリプトタグを閉じて新しいコードを注入できてしまいます。JSONとして埋め込む場合は <code>JSON.stringify</code> した結果の &lt; &gt; &amp; をさらにUnicodeエスケープ(\u003c など)するのが定番の対策です。

ファイルはサーバーに送信されますか?

送信されません。<Link href="/tools/html-escape">HTMLエスケープツール</Link>はすべてブラウザ内(クライアントサイド)で変換を行います。入力したテキストはネットワーク経由で外部に送信されないため、社内コードや機密データを含むテンプレートの変換にも安心して使えます。

Unicode制御文字やゼロ幅スペースもXSSのリスクになりますか?

はい、高度な攻撃ではUnicode制御文字(U+202E RLOなど)やゼロ幅スペース(U+200B)を使って表示を偽装したり、URLやドメインを見間違えさせる手口があります。HTMLエスケープでは防げないため、ユーザー生成コンテンツを表示する場合は制御文字をフィルタリングする処理も併用します。一般的なWebサイトでは5文字のHTMLエスケープ+サニタイズで十分ですが、外部からのリンクやファイル名を表示する場面では注意が必要です。

この記事で紹介したツール