いろいろなコンテクスト用のエスケープ方法を書いてきましたが、HTMLコンテクスト用のエスケープ方法エントリは古いままでした。今のPHPのHTMLエスケープを紹介します。
参考:他のエスケープ方法は以下のエントリを参照してください。
HTMLエスケープ用関数
PHPには2種類のHTMLエスケープ用のエスケープ関数があります。
どちらもHTMLエスケープを行います。HTMLエスケープ用にはどちらを利用すれば良いでしょうか?
基本的にはhtmlspecialchars()を使えば良いです。シングルクオーテーションもエスケープすべきなので
htmlspecialchars($str, ENT_QUOTES);
とエスケープすればOKです。(PHP 5.6以降でデフォルトphp.ini設定の場合)
HTML5用にエスケープしたい場合はENT_HTML5フラグも設定します。
htmlspecialchars($str, ENT_QUOTES|ENT_HTML5);
安全性上、実用上、仕様上からはどちらを使っても問題ありません。’(シングルクオート)のエスケープ方法が変わるだけです。
共通の注意事項
文字エンコーディング設定
PHP 5.6以降をデフォルトphp.ini設定(UTF-8文字エンコーディング)で利用するのであれば文字エンコーディングを設定する必要はありません。具体的にはphp.iniの以下の設定を変更する必要1はありません。
- default_charset
- mbstring.input_encoding
- mbstring.internal_encoding
- mbstring.output_encoding
古いPHPのdefault_charsetはISO-8859-1、mb_ereg系の正規表現関数の文字エンコーディング設定がEUC-JPになっていましたが、PHP 5.6以降はUTF-82になっています。つまり、初期状態でUTF-8に統一されています。
HTMLエスケープ関数も文字エンコーディング引数(第三引数)があります。UTF-8以外の文字エンコーディングを使う場合は一致した文字エンコーディングを渡さなければ問題の原因になります。特にShiftJIS系の文字エンコーディングを利用した場合に正しく設定しないと、簡単にJavaScriptや不正なHTMLをインジェクションされる可能性3があります。
サポートする文字エンコーディング
HTMLエスケープ関数がサポートする文字エンコーディングはmbstringがサポートする文字エンコーディングと異なります。
HTMLエスケープ関数の文字エンコーディング引数は文字エンコーディングのバリデーションに利用されます。エンコーディング変換は行いません。
文字エンコーディングバリデーション
文字エンコーディングはセキュリティ対策として必須です。壊れた文字エンコーディングがあるとインジェクション攻撃が可能になるからです。
HTMLエスケープ関数は文字エンコーディングのバリデーションに失敗すると空の文字列を返します。これは壊れた文字エンコーディングを回復させたり、除去しようとしたりするとインジェクション攻撃に脆弱になることが理由です。
文字エンコーディングバリデーションエラーで空白になる動作はPHPに限った動作ではありません。例えば今のChromeは不正な文字エンコーディングでどうしようもない物は空白ページになるようです。
PHPの文字列型変数はバイナリ型と言えます。文字エンコーディング情報を持っていないため、どのようなバイトデータでも保存します。PHPはdefault_charset設定を使って$_POST/$_GET/$_COOKIEなどのブラウザからの文字エンコーディングを変換4/バリデーションしません。壊れた文字エンコーディングも保存します。
つまり、ユーザーは入力処理で文字エンコーディングをバリデーションしていないと真っ白なページがブラウザに表示されたり、一部が欠けたページが表示される場合があります。データベースやキャッシュシステムなどに壊れた文字エンコーディングが保存されると、恒久的/一時的に真っ白、表示が欠けた壊れたページが表示されます。
このようなDoS状態を確実に防止したい場合は、文字エンコーディングは入力バリデーションしましょう。5
エスケープするクオート文字
HTMLエスケープ関数はデフォルトではダブルクオートしかエスケープしません。しかし、HTML5規格はダブルクオートとシングルクォートの両方をHTML属性値の囲み文字としてサポートしています。
第二引数にENT_QUOTESを設定するとダブルクオートとシングルクォートの両方をエスケープします。必ずENT_QUOTESを設定して利用します。
ただし、ENT_QUOTESだけではHTML4のクオートを利用します。HTML4の'エンティティ化で問題はありませんが、HTML5対応するならENT_HTML5も一緒に指定します。
<?php $str = "<>&'\""; echo htmlspecialchars($str, ENT_QUOTES), PHP_EOL; echo htmlspecialchars($str, ENT_QUOTES|ENT_HTML5);
<>&'" <>&'"
特に理由がなければHTML5対応にした方が見た目が良いので、ENT_HTML5は付けた方が良いと思います。
第二引数のフラグにはENT_SUBSTITUTE(不正な文字エンコーディング文字を置換 – U+FFFD UTF-8の場合、 � 他の文字エンコーディング)、ENT_IGNORE(壊れた文字エンコーディング部分を無視)も利用できます。しかし、一般にはENT_SUBSTITUTE/ENT_IGNOREの利用はすべきではありせん。今時、壊れた文字エンコーディングを送ってくるのは攻撃者しかいません。置換/無視は意図しない動作の原因になる場合があります。不正文字エンコーディングは置換するのではなく、入力処理時にバリデーションし適切に処理すべきです。6
double_encodeオプション
第四引数はdouble_encodeオプションで、HTMLエスケープが複数回適用されている文字列データに対して、複数回のHTMLエスケープ適用を防止するオプションです。既にエスケープされている文字(< >など)は無視してエスケープします。
一般にアプリケーションは多重のエスケープが行われないようにコーディングすべきです。従って、このオプションを通常は使用すべきではありません。
htmlspecialchars()とhtmlentities()の違い
htmlspecialchars()は最小限の文字をエスケープします。
Character | Replacement |
---|---|
&(ampersand) | & |
“ (double quote) | " ENT_NOQUOTES がセットされた場合 |
‘ (single quote) | ' (ENT_HTML401 )または ' ( ENT_XML1 , ENT_XHTML または ENT_HTML5 がセットされた場合), ただしENT_QUOTES もセットされている場合に限る |
< (less than) | < |
> (greater than) | > |
htmlentities()はエンコードするよう設定されたHTMLエンティティに変換します。追加でエンコーディングされる文字は内部的な変換テーブルとして管理されています。この変換テーブルはget_html_translation_table()で取得できます。
<?php var_dump(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES | ENT_HTML5), 'utf-8');
出力は外部サイト(3v4l)で確認してください。この出力から判るようにPHPバージョンによって異なる変換テーブルを持っていることが分かります。
またデフォルト文字エンコーディングのUTF-8の場合、かなり多くの文字がエンティティ化されることも分かります。大きな変換テーブルはより多いCPUの利用を意味します。この為、特に理由がない限り変換文字数が少ないhtmlspecialchars()を利用する方が効率が良いです。7
まとめ
PHPでHTMLコンテクスト用にエスケープする場合、htmlspecialchars()を利用しましょう。
htmlspecialchars($str, ENT_QUOTES|ENT_HTML5);
文字エンコーディングを指定する必要がある場合、文字エンコーディング指定を忘れないでください。指定しないと脆弱になります。
htmlentities()はXMLエスケープ用にも利用できます。
htmlspecialchars($str, ENT_QUOTES|ENT_XML1); // <, >, &, ", ' のみエスケープ htmlentities($str, ENT_QUOTES|ENT_XML1); // XML1変換テーブルを使ってエスケープ
出力対策だけではマトモなアプリケーションは作れません。第一のセキュアコーディング原則を実施が欠かせません。以下のエントリで論理的/原理的に入力検証なしではマトモなアプリケーションは作れないことを説明しています。
- 統一文字エンコーディングとデフォルト文字エンコーディング変更は自分が実装者なのでiconvの文字エンコーディングが漏れていることは解っています。でもiconvを使っているユーザーは居ないですよね?!? ↩
- UTF-8もEUC-JPのISO-8859-1互換文字エンコーディングなので、mbregexのデフォルト文字エンコーディングがEUC-JPだった事に気がつかなかったユーザーも多いと思います。 ↩
- ShiftJISは簡単に攻撃できてしまう状態があります。しかし問題はShiftJIS系の文字エンコーディングに限りません。他の文字エンコーディングでも攻撃が可能になる場合があります。特に、不正バイト列の変換や無視をおこなうと簡単にインジェクションできてしまう場合があります。決して不正バイト列を変換/無視してはなりません。そもそも、文字エンコーディングとして不正なデータは入力処理で廃除するものです。 ↩
- mbstringは入力文字エンコーディングの変換をサポートしています。しかし、現在では特別な理由がない限り利用すべきではありません。 ↩
- 文字エンコーディングのバリデーションは確実に正しく動作するアプリケーションには欠かせません。Webアプリに文字エンコーディングのバリデーションは必要ない、とする意見もあるようですが明らかな間違いです。PHP以外も含め、ほとんどフレームワークで文字エンコーディングバリデーションがないと困った状況になる場合があります。 ↩
- OWASP TOP10 A10(2017)は不正な文字エンコーディングのように攻撃者が送ってくるデータは、検出、ログ、対応(実行を停止し、適切なアラートなどを送信)することを求めています。これを行わないと「不十分なログとモニタリング」脆弱性を持つアプリケーションになります。 ↩
- 古いPHPは文字エンコーディングのバリデーションを行っていませんでした!この為、不正な文字などが含まれる場合に攻撃されるリスクがありました。htmlentities()を利用すると攻撃用文字列などを”ある程度”サニタイズすることをできました。この為、古いPHPではhtmlentities()を利用する方がより安全でした。現在のPHPは文字エンコーディングバリデーションがHTMLエスケープ関数内で実施されるため、一般にhtmlspecialchars()の利用が推奨されます。 ↩