htmlspecialchars($text)は、&、<、>、"、'を、&、<、>、"、'に変換します。ページに出力するユーザーの入力すべてにこれを呼べば、入力はHTMLとして読まれずにテキストとして表示されます。
このブロックは同じコメントを、そのままとエスケープしたものとで2回出力します。実行してPageタブの2行を比べてから、<h1>big</h1>や<img src=x>のように自分でHTMLをフォームに入力してShowを押してください。
そのままの行では、ブラウザはタグに従います。太字の単語は太字になり、<script>と入力した訪問者は、ほかのすべての読者のブラウザで自分のスクリプトを実行させられます。この攻撃はクロスサイトスクリプティング(XSS)と呼ばれます。エスケープした行では、同じ文字が<b>として届き、ブラウザはそれをテキストとして描きます。Outputタブに切り替えると、PHPが実際に出力したエンティティが見えます。
htmlspecialcharsが変換するもの
5つの文字だけで、それ以外はありません。英字、アクセント付きの文字、絵文字はそのまま通ります。
&が一覧に入っているのは、すべてのエンティティが&で始まるからです。そのままにすると、<に触れたコメントが<として表示されてしまいます。
テキストだけでなく属性もエスケープする
属性の中のユーザーの入力も、同じくエスケープが必要です。それがないと、値の中の引用符が属性を閉じ、入力の残りが新しい属性になってしまいます。ここでは「名前」がstyle属性を紛れ込ませています。実行して2つのボックスを見てください。
安全でないボックスでは、ブラウザはvalue="Ada"のあとに新しいstyle属性を見るので、ボックスは赤くなりAdaだけを表示します。攻撃者ならそこにstyleではなくonfocus="..."と書き、自分のコードを実行させるでしょう。安全なボックスではすべての"が"になったので、文字列全体がvalueの中にとどまり、入力したとおりに表示されます。
PHP 8.1以降、デフォルトのフラグはENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401なので、シングルクォートもエスケープされ、'...'で書かれた属性も安全です。古いコードではENT_QUOTESを手で渡していることが多く、PHP 7以前ではそれが必要でした。
テンプレート用の短いヘルパー
テンプレートでhtmlspecialchars($x, ENT_QUOTES, 'UTF-8')を何十回も書くのは煩雑なので、ほとんどのプロジェクトはそれを1文字の関数で包みます。TwigやBladeのようなテンプレートエンジンは、すべての{{ $var }}に対して同じことを自動で行います。
?string型と?? ''が重要です。htmlspecialchars()にnullを渡すのはPHP 8.1以降非推奨で、自分の環境のPHPでは自己紹介のないユーザーごとに非推奨の通知が表示されてしまいます。
二重エンコードとhtmlspecialchars_decode
値が2回エスケープされると、読む人にエンティティが見えてしまいます。&は1回目で&に、2回目で&amp;になり、ブラウザはそれを&と表示します。たいていは、保存するときにエスケープし、出力するときにもう一度エスケープしたことを意味します。既存のエンティティをそのままにするにはdouble_encode: falseを渡し、元に戻すにはhtmlspecialchars_decode()を使います。
本当の解決策は、生のテキストを保存し、出力するときだけエスケープすることです。double_encode: falseは、取り込んだフィードのように、自分が作ったのではないエンティティをすでに含むテキストのためのものです。
htmlspecialchars、htmlentities、strip_tagsの違い
この3つはよく混同されます。このブロックは同じ入力にすべてを実行し、それぞれについてPHPが何を出力し、ブラウザがそれをどう表示するかを示します。
htmlspecialchars()はHTMLの5つの文字をエスケープします。HTMLに出力するすべてのテキストに使います。htmlentities()はéもéに変えます。ページがUTF-8でなかった頃には役立ちましたが、今はソースを読みにくくするだけです。strip_tags()はタグを削除してテキストを残します。HTMLをプレーンテキストにする(メールのプレビュー、メタディスクリプション)ためのもので、セキュリティのためではありません。最後の行が示すように、許可した<b>はonclickを残し、属性の中に置かれたテキストにはまったく触れません。
htmlspecialcharsでは足りない場所
htmlspecialchars()は、HTMLのテキストと引用符で囲んだ属性に対する正しいエスケープです。ページのほかの場所にはほかのルールがあります。
- URLの中では、
http_build_query()かurlencode()が値をエンコードし、そのあとhtmlspecialchars()がパラメータの間の&を正しいHTMLにします。 - JavaScriptの中では、
json_encode()が正しいJSの値を作り、JSON_HEX_TAGが<と>を\u003Cと\u003Eに変えるので、データの中の</script>がscriptタグを閉じることはありません。 - スキームを確認せずにユーザーの入力を
hrefに出力してはいけません。htmlspecialchars('javascript:alert(1)')は何も変わらず、クリックすると実行されます。filter_varのページで示しているように、httpとhttpsのURLだけを受け付けましょう。
これらすべてを組み合わせたフォームの処理は、PHPのフォームを参照してください。
よくある質問
PHPのhtmlspecialcharsは何をしますか?
&、<、>、"、'を、&、<、>、"、'に置き換えます。するとブラウザはそれらの文字をHTMLとして読まずに表示するので、フォームに入力された<script>はテキストとして表示され、決して実行されません。
htmlspecialcharsとhtmlentitiesの違いは何ですか?
htmlspecialchars()が変換するのは、HTMLで特別な意味を持つ5つの文字だけです。htmlentities()は名前付きのエンティティを持つすべての文字も変換するので、caféはcaféになります。UTF-8のページならどちらも同じく安全で、htmlspecialchars()は出力を読みやすく保つので、ふつうはこちらを選びます。
PHP 8でもENT_QUOTESは必要ですか?
安全のためには不要です。PHP 8.1以降、デフォルトのフラグはENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401なので、シングルクォートもエスケープされます。古いバージョンでも同じ動きをし、読む人にも明らかなように、今でも多くのコードは明示的にENT_QUOTES, 'UTF-8'を渡しています。
htmlspecialcharsは入力と出力のどちらで使うべきですか?
出力です。生の値を保存して検証し、HTMLに出力する瞬間にエスケープします。入力時にエスケープすると、データベースに<が保存され、長さや検索が狂い、&lt;のような二重エスケープにつながります。
strip_tagsだけでXSSを防げますか?
防げません。strip_tags()はタグを取り除きますが、許可するタグの引数はその属性を残すので<b onclick="...">は生き残り、属性の中に置かれたテキストには何もしません。ユーザーの入力を出力するときはhtmlspecialchars()を使いましょう。