JavaScript Tips | 文字列ユーティリティ:検索・置換 - HTML エスケープ

JavaScript JavaScript
スポンサーリンク

なぜ「HTML エスケープ」が必要なのか

まず、これだけははっきりさせておきたいです。
HTML エスケープは「見た目を整えるテクニック」ではなく、「セキュリティのための必須処理」です。

ユーザー入力をそのまま HTML に埋め込むと、こういうことが起きます。

入力値: <script>alert('XSS');</script>
そのまま画面に出す: <div><script>alert('XSS');</script></div>
→ ブラウザが script として実行してしまう

これが典型的な XSS(クロスサイトスクリプティング)です。
HTML エスケープは、「HTML として解釈されてしまう文字」を「ただの文字」に変える処理です。


HTML エスケープが具体的にやること

どんな文字を変えるのか

最低限、次の文字はエスケープ対象です。

&&amp;
<&lt;
>&gt;
"&quot;
'&#39;

理由を一つずつ噛み砕きます。

<> はタグの開始・終了に使われるので、そのままだと <script><div> として解釈される。
&&amp;&lt; などの「エンティティ」の開始に使われるので、そのままだと別の意味になる。
"' は属性値の区切りに使われるので、<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, "&amp;") を最初にしているのが重要(後で追加された & を二重にエスケープしないため)。
それぞれの記号を、対応する HTML エンティティに置き換えている。

これで、ユーザー入力を安全に HTML に埋め込めるようになります。


具体例で動きを確認する

危険な入力を「ただの文字」に変える

escapeHtml("<script>alert('XSS');</script>");
// "<script>alert('XSS');</script>"
JavaScript

これを HTML に埋め込むと、ブラウザはこう解釈します。

<div>&lt;script&gt;alert(&#39;XSS&#39;);&lt;/script&gt;</div>

つまり、「<script> という文字列」として表示されるだけで、
スクリプトとしては実行されません。

属性値の中でも安全に使える

const title = `He said "hello" & left.`;

escapeHtml(title);
// "He said "hello" & left."
JavaScript

これを属性に埋め込んでも壊れません。

<div title="He said &quot;hello&quot; &amp; left."></div>

"& がそのまま入っていると、
title="He said "hello" & left." のように途中で途切れてしまいますが、
エスケープしておけば安全です。


どこで HTML エスケープすべきか

原則:「HTML に埋め込む直前」で行う

一番大事なルールはこれです。

「HTML エスケープは、“HTML に埋め込む直前”で行う」

つまり、

データを受け取るとき(API レスポンス・DB からの取得など)にはエスケープしない。
画面に出すとき(innerHTML に入れる、テンプレートに埋め込むなど)にだけエスケープする。

これを守ると、

同じデータを JSON として返すとき
ログに出すとき
別の画面で使うとき

などに、変な「エスケープ済み文字列」が混ざらなくなります。

ダメなパターン:保存時にエスケープしてしまう

例えば、ユーザー入力を DB に保存するときに、
最初から escapeHtml してしまうのはおすすめしません。

理由はシンプルで、

API で JSON を返すときにも &lt; のような文字列が出てしまう。
別の用途(CSV 出力など)で「生の文字列」が欲しいときに困る。
二重エスケープ(&&amp;&amp;amp;)が起きやすい。

だからこそ、「保存するのは“生のデータ”、表示するときだけエスケープ」という設計が大事です。


innerHTML と HTML エスケープ

innerHTML にユーザー入力をそのまま入れてはいけない

これはもう、鉄則です。

const comment = userInput; // ユーザーが入力した文字列

// これは絶対にダメ
element.innerHTML = `<p>${comment}</p>`;
JavaScript

comment<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 に突っ込んでいる状態」から
「セキュリティを意識して設計された文字列ユーティリティ」を持つ状態に、一段レベルアップします。

タイトルとURLをコピーしました