htmlspecialcharsでXSSを防ぐ出力エスケープの原則
htmlspecialcharsは、HTMLの特殊文字をエスケープしてXSSを防ぐ関数です。DBに入れる前ではなく出力する瞬間に、ENT_QUOTESと文字コードを明示して使うのが原則ですね。
エスケープは「出力する瞬間」に行う
XSS 対策の基本は、ユーザー由来の値を HTML に埋め込む瞬間にエスケープする、という一点に尽きると思います。DB に入れる前にエスケープしてしまうと、二重エスケープや別用途での再利用で崩れるので、保存はそのまま、出力時に htmlspecialchars、というのが素直なやり方ですね。
$comment = $_POST['comment'] ?? '';
// 出力する瞬間にエスケープ
echo '<p>' . htmlspecialchars($comment) . '</p>';
ENT_QUOTESでシングルクォートも潰す
ここが見落としやすいのですが、htmlspecialchars はデフォルト(PHP8.1 未満)ではシングルクォートを変換しません。属性値をシングルクォートで囲んでいると、値の中の ‘ で属性を抜け出されてしまうので、必ず ENT_QUOTES を指定する癖をつけたいところです。
// 危険:シングルクォート属性を抜けられる可能性
echo "<input value='" . htmlspecialchars($v) . "'>";
// 安全:シングル・ダブル両方をエスケープ
echo "<input value='" . htmlspecialchars($v, ENT_QUOTES) . "'>";
PHP8.1 からはデフォルトが ENT_QUOTES 相当になりましたが、実行環境のバージョンに依存させたくないので、私は明示するようにしています。
第3引数の文字コードも指定する
第三引数のエンコーディングを省略すると、環境によっては意図しない文字コードで解釈され、不正なバイト列を突かれる余地が残ります。UTF-8 のサイトなら ‘UTF-8’ を明示しておくのが安全ですね。毎回書くのは面倒なのでラッパーを一つ用意しておくと楽です。
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
echo '<td>' . e($user->name) . '</td>';
まとめ
XSS 対策は難しい理屈より、出力時にエスケープする・ENT_QUOTES と文字コードを明示する、という運用を徹底するほうが効くと思います。テンプレートエンジンを使っているなら自動エスケープに任せつつ、生 echo する箇所だけはラッパー関数で統一しておくと抜け漏れが減りますね。
よくある質問
Q. エスケープはいつ行えばいいですか。
A. DBに入れる前ではなく、HTMLに出力する瞬間に行います。保存はそのまま、出力時にhtmlspecialcharsが素直ですね。
Q. ENT_QUOTESはなぜ必要なのですか。
A. これがないとシングルクォートが変換されず、シングルクォートで囲んだ属性値を抜け出される余地が残ります。文字コードと合わせて明示するのが安全です。