HTML エスケープ vs URL エンコード vs Base64 — どれをいつ使うのか
Web 開発で頻出する 3 種類のエスケープ / エンコードを、目的 (安全な埋め込み / 通信路の互換性 / バイナリのテキスト化) と適用範囲 / 文字集合 で比較。誤用パターンも整理します。
「エスケープ」と一口に言うが、3 つは別物
「特殊文字をそのまま書けないので変換する」という処理を、Web では一括りに「エスケープ」と呼びがちです。実際には HTML エスケープ・URL エンコード (percent-encoding)・Base64 は、適用する場所・対象になる文字集合・目的・サイズ膨張率がすべて異なる別の処理です。混同すると、XSS が抜ける、URL に貼った Base64 が壊れる、encodeURI で守ったはずのパラメータが & で割れる、といった事故になります。
判断軸は 4 つです。どの文脈に出力するか (HTML 本文 / URL / バイナリ運搬)、何を無害化するのか (パーサが特別扱いする文字 / 予約文字 / 7-bit ASCII 以外)、結果の可読性 (人間が読むか機械が読むか)、サイズの増え方 (ほぼ等倍か / 数倍に膨らむか)。「とりあえずエスケープしておけば安全」という発想ではなく、出力先が HTML なら HTML エスケープ、URL なら URL エンコード、バイナリ運搬なら Base64、と出力先で決めるのが正解です。
3 つの変換を並べて比較する
| 項目 | HTML エスケープ | URL エンコード | Base64 |
|---|---|---|---|
| 目的 | HTML / XML パーサに特別扱いされる文字を無害化 | URL の予約文字・非 ASCII を安全化 | バイナリを ASCII 64 文字に変換 |
| 対象文字 | < > & " ' | 予約文字 (? & = / # …) と非 ASCII | 任意のバイト列 |
| 出力例 (元 → 変換後) | <a> → <a> | 日本語 → %E6%97%A5%E6%9C%AC%E8%AA%9E | nosend → bm9zZW5k |
| サイズ膨張 | 数文字だけ 5-6 倍、全体ではほぼ等倍 | 非 ASCII は約 3 倍 (UTF-8 3 バイト × 3 文字表現) | 4/3 倍 (約 133%) |
| 可逆性 | 可逆 | 可逆 | 可逆 |
| 仕様 | HTML Living Standard / XML 1.0 | RFC 3986 | RFC 4648 |
| 標準 API | テンプレートエンジン側で自動、または DOM の textContent | encodeURIComponent / encodeURI / decodeURIComponent | btoa / atob (ASCII のみ)、TextEncoder 経由でバイナリ対応 |
| 主用途 | HTML 文書内への文字列埋め込み (XSS 防止) | URL のパス・クエリ・フラグメントへの値埋め込み | メール添付 (MIME)、data URI、JWT、API レスポンスのバイナリ |
サイズ膨張は実運用で効きます。100 KB の画像を data URI で HTML に直書きすると Base64 化で約 133 KB、それを <img src="..."> の属性値として埋めると HTML エスケープがさらに上乗せされ、最終的に gzip 前で約 140 KB 近くになります。URL エンコードは ASCII 文字は手を付けないため英数字パスではほぼ等倍、日本語ファイル名のように非 ASCII を含むと一気に 3 倍に膨らみます。
どこで何を使うか — シーン別の使い分け
フォーム入力をテンプレートに埋め込む: HTML エスケープ。<input value=""> の value や <div></div> の中身に直接ユーザー入力を入れるときに必須です。React / Vue / Astro などモダンなテンプレートエンジンは式中の文字列を自動的にエスケープしますが、dangerouslySetInnerHTML や v-html で抜け道を作ったときだけ自前で気をつける必要があります。
URL にユーザーが書いた文字列を含める: URL エンコード。クエリパラメータ ?q=... に検索語を、パス /users/... にスラッグを入れる場面で encodeURIComponent を使います。?q=A&B と書きたければ ?q=A%26B にしないと、& がパラメータ区切りとして解釈されて値が A だけになります。日本語ファイル名のダウンロード URL も同じ理屈で %E3%83%95%E3%82%A1... 形式に変換します。
画像を HTML に直書き埋め込み: Base64 で data URI (data:image/png;base64,iVBO...) にしてから <img src> に書きます。小さなアイコンやインライン SVG の代替で使えば HTTP リクエストを 1 本減らせますが、3-4 KB を超える画像でやると CSS のキャッシュ効率が落ちて逆効果です。
メール本文・JSON API でバイナリを運ぶ: Base64。SMTP は 7-bit ASCII 前提なので添付ファイルは MIME (RFC 2045) で Base64 化されますし、JSON にバイト列を入れたいときも "image": "iVBORw0KG..." のような文字列にしてから JSON.stringify します。
URL に Base64 を載せたい (JWT、共有リンクのペイロード): 標準 Base64 ではなく Base64URL を使います。+ を - に、/ を _ に、末尾の = パディングを省略する派生形で、URL 上で文字が奪われずに済みます。JWT の header.payload.signature がまさにこの形式です。
ブラウザだけで完結する変換と、混同が招く落とし穴
実務では「サーバーログにある %E3%81%82 の連続を読みたい」「Base64 で来た画像をテキストエディタで見たい」のような変換が頻繁に発生します。URL エンコード / デコードは url-codec が最短で、Base64 のエンコード / デコードは base64 が、Base16 / Base32 / Base58 / Base64URL のあいだを切り替えながら相互変換したいときは base-codec が向いています。どれもブラウザ内で完結し、機密を含む URL や API トークンを貼り付けても外部に送信されません。実装は GitHub で公開しており、DevTools の Network タブで送信ゼロを目視できます。HTML エスケープ専用ツールはサイトに置いていません。テンプレートエンジンや DOM API (textContent への代入で自動エスケープ、innerHTML ではしない) で済むケースがほとんどだからです。
代表的な落とし穴を 4 つ。1 つ目、HTML エスケープを通せば XSS は防げる、は不正確。<a href=""> の属性値の中、<script> タグの中、<style> の中ではコンテキストが違い、必要な対象文字も違います。属性値なら最低でも " を、javascript: URL を許す場面ならスキームのバリデーションを追加で挟みます。2 つ目、encodeURI と encodeURIComponent の取り違え。前者は URL 全体を想定して ? & = / をエスケープしないため、クエリ値に渡すと値の中の & でパラメータが割れます。パラメータ値には encodeURIComponent 一択と覚えてください。3 つ目、URL に標準 Base64 を生で乗せる。+ がスペースに、/ がパス区切りに、= がフォーム終端として奪われます。URL に乗せるなら最初から Base64URL を生成するか、encodeURIComponent を二重に通します (二重通しは可読性が落ちるので前者推奨)。4 つ目、HTML 内のリンクで二重適用を忘れる。<a href="?name=..."> のような場面では、属性値全体に HTML エスケープが、?name= の値に URL エンコードが必要で、両方適用しないとどちらかが抜けます。テンプレートエンジンは HTML エスケープを自動でやってくれても、URL エンコードは自動でやってくれません — そこは自分で encodeURIComponent を通すのが鉄則です。