HTMLエンティティとは、ある文字をマークアップではなくテキストとしてブラウザに表示させるための書き方だ。コンテンツの中に生の < をそのまま書くと、ブラウザはそこからタグの解釈を始めてしまう。代わりに < と書けば、ページ上には文字どおりの < が描画される。この置き換えこそが、HTMLエンティティエンコードの核心だ。
HTMLでは5つの文字が特別な意味を持ち、最も頻繁にエスケープの対象になる。<、>、&、"、' の5つだ。これらをエスケープする理由は2つある。1つは表示のため。コードやマークアップをそのままテキストとして見せたいときだ。もう1つは、こちらのほうが重要だが、セキュリティのため。信頼できない入力をエスケープすることが、クロスサイトスクリプティング(XSS)を防ぐ土台になる。
どのエンティティにも、互いに置き換え可能な3つの書き方がある。名前付き(<)、10進(<)、16進(<)で、いずれも同じ文字に解決される。より厄介なのは、いつエスケープし、何でエスケープするかという問いだ。正しい答えは値が最終的に置かれる場所によって変わるからだ。HTMLテキストなのか、属性なのか、スクリプトなのか、それともURLなのか。記法の違い、予約された文字、文脈ごとの判断、そして多くの人がつまずく落とし穴を、以下で順に扱う。
HTMLエンティティとは何か(構造)
HTMLエンティティは文字参照とも呼ばれ、1文字を表す短いコードだ。すべてのエンティティはアンパサンド & で始まり、セミコロン ; で終わる。その間に何が入るかで、得られる文字が決まる。
形は3種類ある。
&name;— 名前付き参照。<や©のようなもの。&#decimal;— 10進の数値参照。<のようなもの。&#xhex;— 16進の数値参照。<のようなもの。
ブラウザは参照を読み取り、それが指す文字を引き当て、その1文字を描画する。見た目の結果は何も変わらない。< と生の < はまったく同じに表示される。唯一の違いは、エンティティはテキストとして扱われ、決してタグの始まりとは見なされない点だ。
3つの記法:名前付き・10進・16進
3つの記法はいずれも同じUnicodeコードポイントを参照する。違うのは綴り方だけだ。名前付きエンティティは読みやすい形だが、名前が定義されている文字にしか存在しない。10進エンティティはコードポイントを10進数で書く。16進エンティティは同じコードポイントを16進数で書き、Unicode標準で目にする U+XXXX という表記と1対1で対応する。
| Character | Named | Decimal | Hex |
|---|---|---|---|
< | < | < | < |
& | & | & | & |
© | © | © | © |
é | é | é | é |
16進は U+XXXX をそのまま映す――é は U+00E9 なので é になる――ため、特定のコードポイントを文書化したり考察したりするときに多くの開発者が好んで使う。日常的なマークアップでは、名前付きエンティティが最も読みやすい。
エスケープ必須の予約された5文字
これらは、ブラウザの文書解析のしかたを変えてしまうHTML特殊文字だ。実行ではなく表示すべきコンテンツの中にこれらが現れたら、エスケープすること。
| Character | Named | Decimal | Hex | What breaks if you don’t escape it |
|---|---|---|---|---|
< | < | < | < | Starts a tag — the browser reads following text as markup |
> | > | > | > | Closes a tag prematurely |
& | & | & | & | Starts an entity — the rest can be misread as a reference |
" | " | " | " | Ends a double-quoted attribute value early |
' | ' | ' | ' | Ends a single-quoted attribute value early |
HTMLのアンパサンドエンティティは、この仕組み全体の根幹だ。& という文字はすべてのエンティティの始まりなので、真っ先にエスケープしなければならない。アンパサンドより先に山かっこをエスケープすると、いま生成したばかりのエンティティの中の & を再エスケープしてしまう。この落とし穴については後述する。
実際にエスケープが必要なのはいつか(文脈依存)
ここに、ほとんどのバグと、ほとんどの脆弱性が潜んでいる。核となる原則は短い。出力時に、値が置かれる文脈に合わせてエスケープすること。 ある場所では安全な値が、別の場所では危険になる。だから適用するエンコードは、行き先に合わせなければならない。
HTML要素のコンテンツ
<p>、<div>、<td> の中など、タグとタグの間に値を差し込むときは、<、>、& をエスケープする。ここで引用符をエスケープしても害はないが不要だ。<strong> というテキストを、次の単語を太字にするのではなく文字どおりに見せたいなら、<strong> にエンコードする。するとブラウザはタグを適用せず、そのまま印字する。
HTML属性の値
属性の内側では、引用符が決定的に重要になる。値が title="…" の中にあって、エスケープされていない " を含んでいると、属性が早々に終わってしまい、攻撃者が新しい属性を付け足せるようになる。古典的なXSSの攻撃口だ。属性の文脈では "(できれば ' も)をエスケープすること。He said "hi" のような値は、内側に収めておくために He said "hi" にしなければならない。
<script> やインラインJavaScriptの内側
ここではHTMLエンティティは役に立たない。<script> ブロックやインラインのイベントハンドラに組み込まれる文字列には、文字参照ではなくJavaScriptまたはJSONの文字列エスケープが必要だ。JS文字列リテラルの中に " と書いても、引用符ではなくその6文字がそのまま出てくるだけだ。この文脈ではJSONエスケープツールを使い、スクリプト内で実際に効く \uXXXX のルールについてはJSON文字列エスケープ完全ガイドを読んでほしい。
URLの内側
URLには独自のエスケープ方式、すなわちパーセントエンコードがある。HTMLエンティティでは値をURLセーフにできない。a&b c という文字列は、クエリの中では a&b c ではなく a%26b%20c でなければならない。スペースは依然としてURLを壊し、& は依然としてパラメータを区切ってしまうからだ。この用途にはURLエンコード・デコードを使い、予約文字と非予約文字の完全なルールについてはURLエンコード・デコードガイドを参照してほしい。
判断マトリクス
| Context | Escape with | Example | Wrong choice that fails |
|---|---|---|---|
| HTML element content | HTML entities (< > &) | <strong> → <strong> | Leaving < raw injects a tag |
| HTML attribute value | HTML entities (" ' critical) | "hi" → "hi" | An unescaped " breaks out |
<script> / inline JS | JS / JSON string escaping | " → \" | HTML entities are inert in JS |
| URL / query string | Percent-encoding | space → %20 | & and entities still break the URL |
名前付きと数値:どちらを使うべきか
名前付きエンティティは読みやすく、よく使う予約文字やよく知られた記号――<、&、©、—――には妥当な既定の選択だ。ただし、名前が定義されている文字にしか存在しない。数値エンティティは10進でも16進でも、名前のないものを含めてあらゆるコードポイントをエンコードできるため、万能のフォールバックになる。利用側のシステムが特定の名前付きエンティティに対応していると保証できないときは、数値が安全な選択だ。
アポストロフィが ' ではなく ' である理由
名前付きエンティティ ' はHTML5とXMLで初めて導入された。HTML4では未定義なので、一部の古いパーサーやメールクライアントでは、アポストロフィではなく ' という文字どおりのテキストとして描画されてしまう。数値参照の '――そして10進の双子である '――は、まったく同じ文字 U+0027 を指し、これまで書かれたすべての適合パーサーが理解できる。he のようなよくテストされたエスケープライブラリがシングルクォートに ' を出力するのは、まさにこの理由による。優れたエンコーダーもこの慣習に従うので、出力をどんなHTML・XML・属性の文脈に差し込んでも安全だ。
文字セットとエンティティ:非ASCIIをいつエンコードするか
UTF-8のような文字セットは、文字をバイトとしてどう格納するかを決める。エンティティは、プレーンASCII(&、#、;、英字、数字)だけを使って文字を綴る方法だ。両者は別のレイヤーであり、これらを混同すると無駄なエンコードを招く。
UTF-8のページ――つまり <meta charset="utf-8"> を宣言する、ほぼすべての現代的なページ――では、アクセント付き文字、ダッシュ、絵文字は有効な生の文字だ。é、—、😀 はそのまま残しておくこと。すべてをエンティティにエンコードする意味があるのは、テキストがレガシーなシングルバイト文字セットや、生のUTF-8を壊してしまうシステムを通過しなければならないときだけだ。そうした場合のために「すべての非ASCIIをエンコードする」モードが用意されている。バイト・コードポイント・文字の関係がよくわからなければ、UTF-8・UTF-16・Unicodeエンコードガイドがそのモデルを解き明かしてくれる。
よくあるHTMLエンティティの落とし穴
& を最後にエスケープすると二重エスケープになる
順序が重要だ。& より先に < と > を置き換えると、いま作ったばかりのエンティティ(<、>)の先頭の & までエスケープされてしまう。その結果 < は &lt; になり、ページには文字どおりの < というテキストが描画される。必ず & を最初にエスケープし、それから残りを処理すること。このルール1つで、最もありがちなエンコードのバグを防げる。
すでにエスケープ済みのテキストを二重エンコードする
すでにエスケープされているテキストを、もう一度エンコーダーに通すと再エンコードされてしまう。& は &amp; になり、訪問者はページ上で & ではなく & を目にする。エスケープは出力時にちょうど1回だけ行うこと。値が複数の層を通過するなら、そのうち1つだけがエスケープするように徹底する。
デコード時の文字化け
逆方向にもそれ特有の罠がある。誤った文字セットでデコードしたり、二重にデコードしたりすると、出力が崩れる。おなじみの文字化け(mojibake)だ。< を期待していた場所に文字どおりの &lt; が表示されているなら、それをHTMLエンティティデコーダーに貼り付ければ、エンティティが何に解決されるかを正確に確認できる。名前付き、10進、16進はもちろん、末尾のセミコロンがない © のようなレガシーな未終端参照まで扱える。
エスケープをXSSの完全な治療法だと信じる
エスケープは防御の第一線であって、唯一の防御ではない。HTMLには規則の異なる複数の文脈があるため、間違った文脈向けにエスケープすると穴が残る。属性での引用符、スクリプトでのJSエスケープ、URLでのパーセントエンコードといった具合だ。正しい文脈依存のエスケープを、Content Security Policyやフレームワークの自動エスケープと組み合わせること。エンティティエンコードは土台であり、その上にCSPとフレームワークの既定を重ねると考えればいい。
実践でのエンティティのエンコードとデコード
HTMLを手作業で組み立てるときは、自分でエスケープする。以下は & を最優先する順序を正しく扱う escapeHtml() だ。あわせて、実際のアプリケーションコードでのより良い実践も示す。
// 予約された5文字と、その安全なエンティティ:
// < → < > → > & → & " → " ' → '
function escapeHtml(str) {
return str
.replace(/&/g, '&') // & を最初に。後続のエンティティが二重エスケープされないように
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''); // 数値形式 — HTML4・HTML5・XML で安全
}
const userInput = `<a href="x">Tom & Jerry's</a>`;
const safe = escapeHtml(userInput);
// → <a href="x">Tom & Jerry's</a>
// アプリコードではこちらが望ましい: プラットフォームにエスケープを任せる。
// el.textContent = userInput; // ブラウザがエスケープする。手動の置換は不要
// React / Vue / Angular は補間したテキストを既定でエスケープする
// サーバーテンプレート (Jinja, ERB, Blade) はオプトアウトしない限り自動エスケープ
手書きの関数は、何が起きているかを理解するためや、一度きりの変換には役立つ。だが本番では組み込みの経路を優先したい。element.textContent を設定すればブラウザがエスケープしてくれるし、現代のフレームワークは補間した値を自動的にエスケープする。手動のエスケープは、プラットフォームがカバーしないケースのために取っておくこと。
その場かぎりの作業には、HTMLエンティティエンコーダーが予約文字の集合を(名前付き、10進、16進で)エスケープし、HTMLエンティティデコーダーがそれを元に戻す。両者は予約文字について厳密に逆の関係なので、テキストを両方に通しても失われることなく往復できる。
よくある質問
HTMLエンティティとは何ですか?
HTMLエンティティとは、& で始まり ; で終わる短いコードで、1文字を表す。ブラウザはそれをマークアップとして扱わず、エンティティが指す文字を描画する。たとえば < は文字どおりの < を表示し、& は文字どおりの & を表示する。
HTMLでエスケープが必要な文字はどれですか?
予約された5つのHTML特殊文字、<、>、&、"、' だ。要素のコンテンツでは主に <、>、& が必要で、属性の値では引用符 " と ' も決定的に重要になる。アンパサンド & を最初にエスケープして、ほかのエンティティが二重エスケープされないようにすること。
名前付きと数値(10進・16進)のエンティティ、どちらを使うべきですか?
よく使う文字の読みやすさを優先するなら、見分けやすい名前付きエンティティ(<、©)を使う。名前が定義されていない文字をエンコードする必要があるとき、または利用側が特定の名前付きエンティティに対応していると保証できないときは、数値エンティティ(10進 < または16進 <)を使う。どちらの形式も同じコードポイントを参照する。
HTMLエンティティはXSSから守ってくれますか?
正しく適用すれば、土台になる。信頼できない入力をHTMLの要素や属性のコンテンツに置く前に、予約された5文字をエスケープすれば、タグやスクリプトの注入を止められる。ただしエスケープは文脈依存だ。スクリプトブロックにはJavaScriptエスケープが、URLにはパーセントエンコードが必要になる。正しい文脈依存のエスケープを、CSPやフレームワークの自動エスケープと組み合わせること。
ページに < ではなく &lt; が表示されるのはなぜですか?
それは二重エスケープだ。テキストが2回エンコードされたか、山かっこのあとに & がエスケープされたために、< の中の & が & に変わってしまった。その結果、訪問者には < が文字どおりのテキストとして見える。エスケープはちょうど1回だけ行い、常に & を最初にエスケープすること。デコーダーツールを使えば、エンティティが何に解決されるかを確認できる。
é や — や絵文字のような文字をエスケープする必要がありますか?
たいていは不要だ。<meta charset="utf-8"> を宣言するページでは、アクセント付き文字、ダッシュ、絵文字は有効な生の文字であり、エンコードはいらない――そのまま残しておけばいい。非ASCIIをエンコードするのは、テキストがレガシーなシングルバイト文字セットや、生のUTF-8を破壊するシステムを通過しなければならないときだけだ。