HTMLエスケープは「文字列を“ただの文字”として安全に表示する」技
Web画面に文字列を表示するとき、その文字列が「HTMLとして解釈されるか」「ただの文字として扱われるか」は、とても重要です。
<b>重要</b>
これを「太字にしたいHTML」として扱うのか、<b>重要</b> という“そのままの文字”として画面に見せたいのか。
さらに、悪意ある入力があった場合――
<script>alert('XSS');</script>
これをそのまま画面に出してしまうと、ブラウザが「スクリプト」として実行してしまいます。
ここで必要になるのが HTMLエスケープ、つまり「HTMLとして意味を持つ記号を、安全な文字列表現に変換する」技です。
HTMLエスケープの基本:「特別な記号を“無害な文字列”に変える」
代表的なエスケープ対象
HTMLでは、特別な意味を持つ記号があります。
最低限、次の5つは必ず押さえます。
& → &< → <> → >" → "' → '(または ' だが、実務では ' がよく使われる)
例えば、ユーザー入力として <b>重要</b> が来たとき、
これをそのままHTMLに埋め込むと「太字タグ」として解釈されますが、
<b>重要</b>
とエスケープしておけば、ブラウザは「タグ」ではなく「文字」として表示します。
自前実装:シンプルなHTMLエスケープユーティリティ
まずは「5つの基本文字」だけを確実に置き換える
業務で最低限困らないレベルのHTMLエスケープは、自前でも簡単に書けます。
public final class HtmlEscaper {
private HtmlEscaper() {}
public static String escape(String text) {
if (text == null) {
return null;
}
StringBuilder sb = new StringBuilder(text.length() * 2);
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
switch (c) {
case '&':
sb.append("&");
break;
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case '"':
sb.append(""");
break;
case '\'':
sb.append("'");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
}
Java使い方はこうなります。
System.out.println(HtmlEscaper.escape("<b>重要</b>"));
// <b>重要</b>
System.out.println(HtmlEscaper.escape("Tom & Jerry"));
// Tom & Jerry
System.out.println(HtmlEscaper.escape("\"quote\" and 'single'"));
// "quote" and 'single'
Javaここで深掘りしたい重要ポイントは二つです。
一つ目は、「& を必ず最初に、かつ確実に & に変えている」ことです。< や > などのエスケープ済み文字列が混ざっていても、
二重エスケープにならないようにするには、「入力は“生の文字列”である」という前提を守ることが大事です。
(すでにエスケープ済みのものを再度エスケープしない、という運用ルールもセットで必要になります。)
二つ目は、「**エスケープ対象を“最小限に絞っている”」ことです。
HTMLとして意味を持つ記号だけを変換し、それ以外はそのまま通します。
これにより、「日本語や記号が変な文字列になる」といった副作用を避けられます。
例題:ユーザー入力をそのまま画面に出すときの危険と対策
「入力値をそのままHTMLに埋め込む」は危険
例えば、こんなコードを考えてみます。
String name = request.getParameter("name");
out.println("<p>こんにちは、" + name + " さん</p>");
Javaここで、ユーザーが次のような値を送ってきたらどうなるでしょう。
<script>alert('XSS');</script>
そのままHTMLに埋め込まれると、ブラウザはこれをスクリプトとして実行してしまいます。
これが典型的な XSS(クロスサイトスクリプティング)です。
これを防ぐには、画面に出す直前で必ずHTMLエスケープする必要があります。
String name = request.getParameter("name");
String safeName = HtmlEscaper.escape(name);
out.println("<p>こんにちは、" + safeName + " さん</p>");
Javaこうしておけば、画面には次のように表示されます。
<p>こんにちは、<script>alert('XSS');</script> さん</p>
ブラウザはこれを「ただの文字」として扱うので、スクリプトは実行されません。
ここでの重要ポイントは、「“入力時”ではなく“出力時(HTMLに埋め込む直前)”にエスケープする」という設計です。
入力の段階でエスケープしてしまうと、
「DBにはエスケープ済み」「ログには生の値」など状態がバラバラになり、
どこが安全でどこが危険なのか分かりにくくなります。
例題:属性値の中に埋め込むときの注意点
<input value="ここに入る"> のようなケース
HTMLエスケープは、「テキストノード」と「属性値」で少し意味合いが変わります。
例えば、次のようなHTMLを出力したいとします。
<input type="text" name="q" value="ユーザー入力">
ここで value にユーザー入力を埋め込むときも、必ずエスケープが必要です。
String q = request.getParameter("q");
String safeQ = HtmlEscaper.escape(q);
out.println("<input type=\"text\" name=\"q\" value=\"" + safeQ + "\">");
Javaもしエスケープしないと、こんな入力で壊れます。
" onfocus="alert('XSS')
これがそのまま入ると、HTMLはこうなります。
<input type="text" name="q" value="" onfocus="alert('XSS')">
属性が途中で閉じられ、意図しない属性が追加されてしまいます。
" や ' を " や ' にエスケープしておけば、
ブラウザはそれを「属性の区切り」ではなく「ただの文字」として扱います。
ここでのポイントは、「テキストノードでも属性値でも、“HTMLとして意味を持つ記号”は必ずエスケープする」ということです。<, >, &, ", ' の5つを押さえておけば、基本的なXSS対策としてはかなり強くなります。
ライブラリを使う場合との違いと使い分け
Apache Commons Text / Spring などの既存実装
実務では、自前実装ではなくライブラリを使うことも多いです。
例えば、Apache Commons Text には StringEscapeUtils.escapeHtml4 があり、
HTML4仕様に沿ったエスケープをしてくれます。
import org.apache.commons.text.StringEscapeUtils;
String safe = StringEscapeUtils.escapeHtml4("<b>重要</b> & \"test\"");
System.out.println(safe);
// <b>重要</b> & "test"
Javaライブラリを使うメリットは、
- 仕様に沿った細かいエスケープ(特殊文字やサロゲートペアなど)を面倒見てくれる
- 自前実装のバグ(抜け漏れ・二重エスケープなど)を減らせる
といったところです。
一方で、自前ユーティリティを持つメリットもあります。
- プロジェクトごとのルール(どこまでエスケープするか)を明示できる
- ライブラリに依存しない軽量な処理が書ける
- テストしやすく、挙動を完全に把握できる
現場では、「基本はライブラリを使うが、テストや学習用に自前実装も理解しておく」というスタンスが一番強いです。
まとめ:HTMLエスケープユーティリティで身につけたい感覚
HTMLエスケープは、「文字列を“HTMLとして解釈させないようにする”」ための、安全面で非常に重要なテクニックです。
押さえておきたい感覚は、まず「<, >, &, ", ' の5つは必ずエスケープする」ということ。
次に、「入力時ではなく“HTMLに出力する直前”でエスケープする」という設計を徹底すること。
そして、「テキストノードでも属性値でも、HTMLとして意味を持つ記号はすべて“ただの文字”に変換する」という意識です。
