1. ホーム
  2. PHP

【PHP】htmlspecialchars の使い方|HTMLエスケープでXSSを防ぐ

Share

フォームに入力された名前やコメントを、そのまま画面に表示していませんか。ユーザーが入力した文字をエスケープせずに出力すると、表示が崩れたり、最悪の場合はスクリプトを埋め込まれる「XSS(クロスサイトスクリプティング)」という攻撃を許してしまいます。これを防ぐ基本が htmlspecialchars です。この記事では、なぜエスケープが必要なのか、htmlspecialchars の基本構文と引数の意味、実践的な使い方、htmlentities との違いまでを、PHP を学び始めた方にも分かるように解説します。

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

HTML では <> がタグの始まりと終わりを表し、& は特殊文字の起点になります。ユーザーが入力した文字列にこれらが含まれたまま画面へ出力すると、ブラウザはそれを「ただの文字」ではなく「HTMLの一部」として解釈してしまいます。

たとえば、コメント欄に <script>alert('XSS')</script> と入力された値をそのまま出力すると、そのスクリプトが実行されてしまいます。これが XSS です。攻撃者は他人のページに任意のスクリプトを埋め込み、Cookie を盗んだり、なりすましの操作をさせたりできます。htmlspecialchars を通すと、<&lt; という「表示用の文字」に変換され、ブラウザはタグではなく文字として安全に表示します。

index.php
<?php
$comment = "<script>alert('XSS')</script>";

// そのまま出力すると script タグとして実行されてしまう(危険)
echo $comment;

// htmlspecialchars を通すと、文字としてそのまま表示される(安全)
echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
// 出力: &lt;script&gt;alert(&#039;XSS&#039;)&lt;/script&gt;

原則として、ユーザー入力やデータベースから取り出した値を HTML に出力するときは、必ずエスケープすると覚えておきましょう。これは攻撃対策であると同時に、記号がそのまま正しく表示されるという表示崩れの防止にもなります。

htmlspecialcharsの基本構文

htmlspecialchars は、HTML で特別な意味を持つ少数の文字を、対応する文字実体参照(エンティティ)に変換して返す関数です。基本の構文は次のとおりです。

syntax.php
<?php
// htmlspecialchars(変換する文字列, フラグ, 文字エンコーディング)
$safe = htmlspecialchars($string, ENT_QUOTES, 'UTF-8');

第1引数に変換したい文字列を渡すだけでも動きますが、安全のためには第2引数のフラグと第3引数のエンコーディングも明示しておくのが確実です。まず、この関数が何を変換するのかを見てみましょう。デフォルトで変換される文字は次の5つです(シングルクォートはフラグ次第)。

変換前変換後説明
&&amp;アンパサンド(常に変換)
<&lt;小なり(常に変換)
>&gt;大なり(常に変換)
"&quot;ダブルクォート(ENT_NOQUOTES のときは変換しない)
'&#039;シングルクォート(ENT_QUOTES または ENT_HTML5 指定時のみ変換)

このように、変換されるのはごく一部の記号だけです。日本語などのマルチバイト文字はそのまま残るため、文章の見た目を壊さずに危険な記号だけを無害化できます。

第2引数のフラグでクォートの扱いを決める

第2引数のフラグは、クォート(引用符)をどう扱うかを指定します。代表的なフラグは次のとおりです。

フラグダブルクォートシングルクォート
ENT_QUOTES変換する変換する
ENT_COMPAT変換する変換しない
ENT_NOQUOTES変換しない変換しない

このうち 実務では ENT_QUOTES を使うのが基本です。理由は、HTML の属性値はダブルクォートだけでなくシングルクォートで囲むこともあるからです。たとえば属性を id='...' のようにシングルクォートで囲んでいる場合、シングルクォートがエスケープされないと、ユーザー入力で属性を抜け出され、別の属性やイベントハンドラを差し込まれる余地が生まれます。ENT_QUOTES なら両方のクォートを変換するため、どちらで囲んでも安全です。

なお、第2引数を省略したときのデフォルトは、PHP 8.1 より前は ENT_COMPAT 相当でダブルクォートのみが変換され、シングルクォートは残っていました。PHP 8.1 以降はデフォルトが ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 に変更され、シングルクォートもデフォルトで変換されるようになっています。とはいえ動作するバージョンに依存しないよう、フラグは省略せず ENT_QUOTES を明示する習慣をつけておくと安心です。

第3引数の文字エンコーディングを明示する

第3引数は、入力文字列の文字エンコーディングを指定します。現在の Web では UTF-8 が標準なので、'UTF-8' を明示しておきます。

エンコーディングを指定する理由は、不正なバイト列が混ざっていたときの挙動に関わるからです。指定したエンコーディングとして無効なバイト列が含まれていると、htmlspecialchars は空文字列を返すことがあります(ENT_SUBSTITUTE フラグを併用すると、不正な文字を置換文字に変換できます)。エンコーディングを省略すると環境依存のデフォルトが使われ、意図しない結果になることがあるため、UTF-8 で扱っているなら必ず 'UTF-8' と書いておきましょう。PHP 5.4 以降は省略時のデフォルトが UTF-8 ですが、明示しておくほうがコードの意図が明確になります。

encoding.php
<?php
$name = '田中 <太郎>';

// 第3引数で UTF-8 を明示しておく
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// 出力: 田中 &lt;太郎&gt;  (日本語はそのまま、記号だけ変換)

フォーム入力を安全に表示する

もっとも多い使い方が、送信されたフォームの値を画面に出し直すケースです。たとえば入力内容の確認画面や、エラー時に入力値を保持して再表示する場面です。毎回 htmlspecialchars を書くのは冗長なので、短いヘルパー関数を用意しておくと読みやすくなります。

form.php
<?php
// 出力用のエスケープをまとめたヘルパー
function h($string) {
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}

// 送信された値を受け取る(未送信なら空文字)
$name    = $_POST['name']    ?? '';
$comment = $_POST['comment'] ?? '';
?>

<p>お名前: <?= h($name) ?></p>
<p>コメント: <?= h($comment) ?></p>

<?= ... ?><?php echo ... ?> の短縮形です。出力する瞬間に h() を通すことで、入力に <script> などが含まれていても、ただの文字として安全に表示されます。ポイントは、データを保存するときではなく、HTMLとして出力する直前にエスケープすることです。こうしておくと、同じデータをメール本文や JSON など別の形式で使うときにも元の値をそのまま利用できます。

属性値の中で使うときの注意

エスケープが必要なのはタグの中身(テキスト)だけではありません。valuehref など、HTML 属性の値にユーザー入力を埋め込むときも同じくエスケープが必要です。ここでクォートのエスケープが効いてきます。

attribute.php
<?php
// 攻撃者が value を抜け出してイベントを差し込もうとする入力例
$keyword = '" onmouseover="alert(1)';

// ENT_QUOTES なのでダブルクォートが &quot; に変換され、属性を抜け出せない
echo '<input type="text" value="'
   . htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8')
   . '">';
// 出力: <input type="text" value="&quot; onmouseover=&quot;alert(1)">

もしクォートをエスケープしないと、value="..." の閉じクォートが入力側のダブルクォートと一致してしまい、その後ろの onmouseover=... が新しい属性として解釈されてしまいます。ENT_QUOTES でクォートを変換しておけば、属性値の中に閉じ込められたままになり、こうした抜け出しを防げます。属性値は必ずクォートで囲み、その中身は htmlspecialchars でエスケープする、とセットで覚えておきましょう。

htmlspecialcharsとhtmlentitiesの違い

似た関数に htmlentities があります。引数の形は同じですが、変換する範囲が異なります。違いを整理すると次のとおりです。

項目htmlspecialcharshtmlentities
変換する文字& < > " ' のみ上記に加え、実体参照を持つすべての文字
日本語などの扱いそのまま残すエンコーディング次第で実体参照に変換されうる
主な用途XSS対策・表示崩れ防止特殊な記号を実体参照に統一したいとき

XSS 対策や通常の表示で必要なのは、HTML として意味を持つ少数の記号を無害化することだけです。その目的には htmlspecialchars で十分であり、出力サイズも小さく済みます。htmlentities は変換対象が広く、用途が限られるため、セキュリティ目的なら htmlspecialchars を選ぶのが基本です。

エスケープした文字列を元に戻す

変換した文字列を元の記号に戻したいときは、htmlspecialchars_decode を使います。&lt;< に、&amp;& に戻すといった逆変換を行います。第2引数には、エンコード時と同じフラグを渡すのが基本です。

decode.php
<?php
$encoded = htmlspecialchars("<b>太字</b>", ENT_QUOTES, 'UTF-8');
// $encoded は "&lt;b&gt;太字&lt;/b&gt;"

$decoded = htmlspecialchars_decode($encoded, ENT_QUOTES);
echo $decoded;
// 出力: <b>太字</b>

ただし、デコードした文字列をそのまま HTML に出力すると、再びタグとして解釈されて XSS の入り口になります。デコードは「メール本文として平文で使う」「別の処理に渡す」といった、HTML として出力しない場面に限って使いましょう。

二重エスケープに気をつける

つまずきやすいのが、すでにエスケープ済みの文字列をもう一度エスケープしてしまう「二重エスケープ」です。htmlspecialchars& を変換するため、&amp; のような文字実体参照に含まれる & がさらに &amp;amp; になってしまいます。

double-escape.php
<?php
$text = 'Tom & Jerry';

$once  = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
// "Tom &amp; Jerry"

// すでにエスケープ済みの文字列をもう一度通すと…
$twice = htmlspecialchars($once, ENT_QUOTES, 'UTF-8');
// "Tom &amp;amp; Jerry"  ← 画面に「&amp;」と表示されてしまう

この状態だと、画面に Tom &amp; Jerry のように &amp; がそのまま見えてしまいます。防ぐコツは、エスケープは出力する1回だけにすることです。データを受け取った時点や保存時にはエスケープせず生の値のまま扱い、HTML へ出力する最後の瞬間にだけ htmlspecialchars を通す、という流れを徹底すれば二重エスケープは起きません。どうしても二重エスケープを避けたいライブラリ的な処理では、第4引数 $double_encodefalse を渡すと既存の実体参照を再変換しないようにできますが、まずは「出力時に1回だけ」を基本にしましょう。

まとめ

htmlspecialchars は、& < > " ' といった HTML で特別な意味を持つ記号を文字実体参照に変換し、XSS や表示崩れを防ぐための基本的な関数です。実務では第2引数に ENT_QUOTES、第3引数に 'UTF-8' を明示し、シングルクォートも含めて確実にエスケープするのが安全です。エスケープはデータの保存時ではなく HTML へ出力する直前に1回だけ行い、属性値の中でもクォートで囲んだうえでエスケープする、という流れを守れば、ユーザー入力を安心して画面に表示できます。逆変換の htmlspecialchars_decode は HTML として出力しない場面に限って使い、二重エスケープに注意してください。

参考ページ