なぜ「HTML エスケープ」が必要なのか
まず、これだけははっきりさせておきたいです。
HTML エスケープは「見た目を整えるテクニック」ではなく、「セキュリティのための必須処理」です。
ユーザー入力をそのまま HTML に埋め込むと、こういうことが起きます。
入力値: <script>alert('XSS');</script>
そのまま画面に出す: <div><script>alert('XSS');</script></div>
→ ブラウザが script として実行してしまう
これが典型的な XSS(クロスサイトスクリプティング)です。
HTML エスケープは、「HTML として解釈されてしまう文字」を「ただの文字」に変える処理です。
HTML エスケープが具体的にやること
どんな文字を変えるのか
最低限、次の文字はエスケープ対象です。
& → &< → <> → >" → "' → '
理由を一つずつ噛み砕きます。
< と > はタグの開始・終了に使われるので、そのままだと <script> や <div> として解釈される。& は & や < などの「エンティティ」の開始に使われるので、そのままだと別の意味になる。" と ' は属性値の区切りに使われるので、<div title="..."> の中で壊れやすい。
これらを「エンティティ」と呼ばれる形に変換することで、
ブラウザに「これはタグじゃなくて、ただの文字だよ」と伝えます。
シンプルな HTML エスケープユーティリティ
実装例
まずは、業務で普通に使えるレベルのシンプルな関数を書いてみます。
function escapeHtml(value) {
if (value == null) return "";
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
JavaScriptやっていることを順番に噛み砕きます。
null / undefined が来ても落ちないように、最初にチェックしている。String(value) で文字列に変換してから置換している。replace(/&/g, "&") を最初にしているのが重要(後で追加された & を二重にエスケープしないため)。
それぞれの記号を、対応する HTML エンティティに置き換えている。
これで、ユーザー入力を安全に HTML に埋め込めるようになります。
具体例で動きを確認する
危険な入力を「ただの文字」に変える
escapeHtml("<script>alert('XSS');</script>");
// "<script>alert('XSS');</script>"
JavaScriptこれを HTML に埋め込むと、ブラウザはこう解釈します。
<div><script>alert('XSS');</script></div>
つまり、「<script> という文字列」として表示されるだけで、
スクリプトとしては実行されません。
属性値の中でも安全に使える
const title = `He said "hello" & left.`;
escapeHtml(title);
// "He said "hello" & left."
JavaScriptこれを属性に埋め込んでも壊れません。
<div title="He said "hello" & left."></div>
" や & がそのまま入っていると、title="He said "hello" & left." のように途中で途切れてしまいますが、
エスケープしておけば安全です。
どこで HTML エスケープすべきか
原則:「HTML に埋め込む直前」で行う
一番大事なルールはこれです。
「HTML エスケープは、“HTML に埋め込む直前”で行う」
つまり、
データを受け取るとき(API レスポンス・DB からの取得など)にはエスケープしない。
画面に出すとき(innerHTML に入れる、テンプレートに埋め込むなど)にだけエスケープする。
これを守ると、
同じデータを JSON として返すとき
ログに出すとき
別の画面で使うとき
などに、変な「エスケープ済み文字列」が混ざらなくなります。
ダメなパターン:保存時にエスケープしてしまう
例えば、ユーザー入力を DB に保存するときに、
最初から escapeHtml してしまうのはおすすめしません。
理由はシンプルで、
API で JSON を返すときにも < のような文字列が出てしまう。
別の用途(CSV 出力など)で「生の文字列」が欲しいときに困る。
二重エスケープ(& → & → &amp;)が起きやすい。
だからこそ、「保存するのは“生のデータ”、表示するときだけエスケープ」という設計が大事です。
innerHTML と HTML エスケープ
innerHTML にユーザー入力をそのまま入れてはいけない
これはもう、鉄則です。
const comment = userInput; // ユーザーが入力した文字列
// これは絶対にダメ
element.innerHTML = `<p>${comment}</p>`;
JavaScriptcomment に <script>...</script> が入っていたら、
そのまま実行されてしまいます。
escapeHtml を通してから埋め込む
const comment = userInput;
element.innerHTML = `<p>${escapeHtml(comment)}</p>`;
JavaScriptこうすると、<script> も </p> も「ただの文字」として扱われます。
もし「HTML として解釈させたい安全な文字列」(自分たちが生成したテンプレートなど)と、
「ユーザー入力」が混ざる場合は、
ユーザー入力の部分だけ必ず escapeHtml を通す、という癖をつけてください。
HTML エスケープユーティリティをどう設計するか
まずは「escapeHtml」をプロジェクトの共通関数にする
プロジェクトのどこからでも使える場所に、
さっきの escapeHtml を一つ置いておきます。
export function escapeHtml(value) {
if (value == null) return "";
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
JavaScriptそして、
テンプレート文字列で HTML を組み立てるとき
innerHTML に代入するとき
サーバーサイドレンダリングで HTML を出力するとき
には、「ユーザー入力が混ざるところは必ず escapeHtml を通す」というルールにします。
「エスケープ済みかどうか」を混ぜない
一番ややこしくなるのは、
この文字列はエスケープ済み
この文字列は未エスケープ
が混ざる状態です。
なので、
保存しているデータは「未エスケープ」で統一する。
HTML に出すときだけ「エスケープ済み」にする。
という線引きを、チームとして共有しておくと、
「ここはもうエスケープされてるんだっけ?」という迷いが減ります。
ちょっとだけ手を動かしてみる
コンソールで、次のあたりを試してみてください。
escapeHtml("<b>太字</b>");
escapeHtml(`He said "hello" & left.`);
escapeHtml("<script>alert('XSS');</script>");
JavaScriptそして、ブラウザの開発者ツールで、
それを innerHTML に入れたときにどう表示されるかを見てみてください。
「タグとして解釈される」のと
「ただの文字として表示される」の違いが、
目で見て分かるようになると、HTML エスケープの重要性が一気に腑に落ちます。
そのうえで、自分のプロジェクトに
export function escapeHtml(value) { ... }
JavaScriptを一つ置いて、
「ユーザー入力を HTML に埋め込むときは、必ずここを通す」
というルールにしてみてください。
それができた瞬間、あなたのコードは
「なんとなく innerHTML に突っ込んでいる状態」から
「セキュリティを意識して設計された文字列ユーティリティ」を持つ状態に、一段レベルアップします。
