「HTML アンエスケープ」とは何をするものか
前回の「HTML エスケープ」は、< や & などを「タグとして解釈されないように」安全な文字列に変える処理でした。
"<b>太字</b>" → "<b>太字</b>"
HTML アンエスケープは、その逆方向です。
"<b>太字</b>" → "<b>太字</b>"
"Tom & Jerry" → "Tom & Jerry"
「エスケープされた文字列」を「元の記号に戻す」処理が、HTML アンエスケープです。
どんな場面で HTML アンエスケープが必要になるのか
すでにエスケープされた文字列を「人間が読む形」に戻したいとき
ログやデータベースに、
すでにエスケープ済みの文字列が保存されていることがあります。
タイトル: <重要> お知らせ
本文: Tom & Jerry の新作です
これを画面上で「そのままの文字」として見せたいとき、< や & のままだと読みにくいので、< や & に戻したくなります。
このときに使うのが HTML アンエスケープです。
「テキストとして扱いたい」けれど、元の記号が欲しいとき
例えば、ユーザーに
入力例: <b>太字</b>
と表示したいのに、
データとしては <b>太字</b> が渡されている場合、
アンエスケープしてから表示したほうが自然です。
まずは「エスケープ」と「アンエスケープ」の対応を整理する
HTML エスケープでよく使う変換は、だいたいこうでした。
& → &< → <> → >" → "' → '
アンエスケープは、この逆をやります。
& → &< → <> → >" → "' → '
この対応関係を、まず頭に置いておいてください。
シンプルな HTML アンエスケープユーティリティ
実装例
業務でよく使うレベルの、素直なアンエスケープ関数を書いてみます。
function unescapeHtml(value) {
if (value == null) return "";
return String(value)
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, "\"")
.replace(/'/g, "'")
.replace(/&/g, "&");
}
JavaScriptポイントを噛み砕きます。
null / undefined が来ても落ちないようにしている。String(value) で文字列にしてから置換している。& の置換を一番最後にしている(先にやると、他の < などが壊れるから)。
よく使う5種類のエンティティだけを対象にしている。
これで、基本的なアンエスケープは十分こなせます。
具体例で動きを確認する
タグっぽい文字列を元に戻す
unescapeHtml("<b>太字</b>");
// "<b>太字</b>"
JavaScriptこれは「タグとして解釈させたい」わけではなく、
「<b>太字</b> という文字列をそのまま見せたい」ケースで使います。
記号を含むテキストを元に戻す
unescapeHtml("Tom & Jerry");
// "Tom & Jerry"
unescapeHtml("He said "hello".");
// "He said \"hello\"."
JavaScript& や " が、
人間にとって自然な & や " に戻っているのが分かると思います。
重要な注意点:アンエスケープしたものをそのまま HTML に入れない
ここが一番大事なポイントです。
HTML アンエスケープは、「安全にする処理」ではありません。
むしろ、場合によっては「危険に戻す処理」です。
例えば、次のような文字列があったとします。
"<script>alert('XSS');</script>"
これをアンエスケープすると、
unescapeHtml("<script>alert('XSS');</script>");
// "<script>alert('XSS');</script>"
JavaScriptこれをそのまま innerHTML に入れたら、
普通にスクリプトが実行されてしまいます。
// 絶対にやってはいけない例
element.innerHTML = unescapeHtml(storedValue);
JavaScriptなので、
アンエスケープしたものを「HTML として解釈させる」のは、基本的に危険。
アンエスケープは「テキストとして見せたいとき」にだけ使う。
という線引きを、しっかり意識しておいてください。
どんなときに「アンエスケープしてから表示」してよいか
「HTML として解釈させない」表示方法なら OK
例えば、textContent や innerText に入れる場合は、
ブラウザは中身を「ただのテキスト」として扱います。
const raw = "<b>太字</b>";
element.textContent = unescapeHtml(raw);
// 画面には <b>太字</b> と“そのまま”表示される(タグとしては解釈されない)
JavaScriptこの場合は、アンエスケープしても XSS にはなりません。
「人間にとって読みやすい形に戻す」用途として、素直に使えます。
ログやダウンロード用テキストなど
ログファイルに出力するときや、
テキストファイルとしてダウンロードさせるときなども、
「HTML として解釈されない」ので、アンエスケープして構いません。
HTML エスケープとアンエスケープの設計ルール
保存するのは「生のデータ」か「エスケープ済み」か
理想は、
DB や API では「生のデータ」を扱う。
画面に出すときだけ escapeHtml する。
です。
しかし、現実には「すでにエスケープ済みのデータが保存されている」システムもあります。
その場合、
画面に出すときに「二重エスケープ」になっていないか。
API で返すときに「エスケープ済み文字列」を返していないか。
などを確認する必要があります。
アンエスケープユーティリティは、
そういった「歴史的事情のあるデータ」を扱うときの“救済手段”として使うことが多いです。
「エスケープ」と「アンエスケープ」を混乱させない
プロジェクトのどこかに、共通のユーティリティとして
export function escapeHtml(value) { ... }
export function unescapeHtml(value) { ... }
JavaScriptを置いておき、
「どこでどちらを使うか」をチームで共有しておくと、
「ここはもうエスケープされてるんだっけ?」という混乱が減ります。
ちょっとだけ手を動かしてみる
コンソールで、次のあたりを試してみてください。
unescapeHtml("<b>太字</b>");
unescapeHtml("Tom & Jerry");
unescapeHtml("He said "hello" & left.");
JavaScriptそして、escapeHtml と組み合わせて、
const original = "<b>太字</b>";
const escaped = escapeHtml(original);
const unescaped = unescapeHtml(escaped);
JavaScriptを試してみてください。
original → escaped → unescaped が
どう変わって、どう元に戻るかを確認すると、
「エスケープ」と「アンエスケープ」の役割がかなりクリアになります。
そのうえで、自分のプロジェクトに
export function escapeHtml(value) { ... }
export function unescapeHtml(value) { ... }
JavaScriptを一つ置いて、
「HTML に埋め込むときは escapeHtml」
「テキストとして読みやすくしたいときだけ unescapeHtml(ただし HTML としては解釈させない)」
というルールにしてみてください。
それができた瞬間、あなたの文字列処理は
「なんとなく < と & をいじっている状態」から
「セキュリティと可読性を意識した設計」に、一段レベルアップします。
