最近、 DBに保存されているフォーム(HTMLコード)と、これまたDBに保存したユーザーの回答データを画面上に表示する、という機能に触ることがありました。
その時にHTMLエンティティ変換の煩わしさとXSSの沼にハマったので、配慮するべきポイントについてまとめておきます。
{!! $html !!}について
// $html = '<p>XSS</p>'
//
{!! $html !!}
// $xss = "<script>alert('かかったな!');</script>";
{!! $xss !!}
↑のようにすることで展開した変数が文字ではなくコードとして意味を持ち得るコードになる。
``
DBから取得し、変数に格納したHTMLコード
この辺りは公式:エスケープしないデータの表示
①HTMLエスケープはhtmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode)
htmlspecialcharsはENT_QUOTES
を付けないとシングルクォート ` をエスケープしてくれません。
$no_escape = htmlspecialchars($single_quote);
echo $no_escape;
// ' と表示
$escaped = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
echo $escape;
// ' と表示(HTML上では ` で表示)
HTMLでは値の区切りに"
を使うのがセオリーなのですが、シングルクォートも使えます。
<?php $xss = '\'/><script>alert(\'XSS\');</script>'; ?>
<input type="text" value='<?php echo $xss; ?>'>
誰かがうっかりシングルクォートを使ったフォームを作成してしまってもいいように、ENT_QUOTES
は忘れずに付けましょう。
Laravelのe()を使うor参考にする
Laravelを使っているならヘルパ関数e()
を使えばこの辺りに配慮した対応をしてくれます。
使ってない方でも、e()の中身を参考にHTMLエスケープするといいでしょう。
if (! function_exists('e')) {
/**
* Encode HTML special characters in a string.
*
* @param \Illuminate\Contracts\Support\Htmlable|string $value
* @param bool $doubleEncode
* @return string
*/
function e($value, $doubleEncode = true)
{
if ($value instanceof Htmlable) {
return $value->toHtml();
}
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode);
}
}
ちなみに、第3引数のエンコーディング規格を指定しない場合、各バージョンのPHPによって自動的に規格が適用されます。
公式では明示的に指定することを推奨しています。
②htmlentities()は基本、使わない
htmlentities()も文字列をHTMLエンティティにしてくれますが、コードとしての意味を持たない(XSSリスクに関係ない)文字列まで変換します。
参考:PHPのhtmlentities()とhtmlspecialchars()の違いと適切なエンティティ変換
NGではないですが、オーバースペックな関数です。
htmlspecialchars()とデコードのための関数が異なる=デコード間違いによるバグのリスクを生む可能性があるため、特別な理由がない限りは使わないでいいでしょう。
③適切なデコード関数を使うはhtmlspecialchar_decode()を使う
htmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode);
でデコードした文字列はhtmlspecialchar_decode()
を使ってデコードしましょう。ENT_QUOTE
を忘れずに!
htmlspecialchar_decode($str, ENT_QUOTE);
②のhtmlentities()
を使った文字列に対してはhtml_entity_decodeを使います。
html_entity_decode($str, ENT_QUOTE);
④DBに保存する情報はそのままでOK
'><script>alert('XSS');</script>
みたいな入力がされたからといって、それが即有害な文字列だとはいい切れません。
DBにvarchar型で保存されたり、CSVファイルに出力される分には実害はありません。情報はHTMLにレンダリングされるとは限らないのです。
XSSに用いられるような文字列表現が無条件に有害とはいえない、といえます。
(その意味で、サニタイズ(消毒)という言葉は不適切であり、セキュリティワードに繊細な方からは敬遠されているんだとか)
仮にDBから引っ張ってきたエスケープ文字列をvue.jsのdata()に入れて表示させると、エスケープされたままの文字列が表示されてしまいます。
文字列をエスケープした状態でDBに保存したりすると、HTML以外に出力する時全てのケースでデコードしなければならないので、対応コストが爆増します。
DBに保存する情報はそのままでOK、必要に応じてエンコード処理を加えるという考えでいきましょう。
⑤inputのvalueに入る値にもHTMLエンティティは有効になる
<input type="text" value="& ">
// input内に & に変換、表示される
valueに入る時もHTMLエンティティは文字列に変換されます。
初歩的なことかもしれませんが、考えすぎてエスケープ処理がゲシュタルト崩壊してくると判断が付かなくなったので、備忘録として。
参考資料
サニタイズ/入力値検証/エスケープの考え方
IPA 安全なウェブサイトの作り方
悪いサニタイズ、そして良い(?)サニタイズ、そして例外処理
PHPのhtmlentities()とhtmlspecialchars()の違いと適切なエンティティ変換
まとめ
調べれば調べるほど{!! $html !!}
を使うこと自体がアンチパターンじゃないの?と思えてきますね。
とはいえ、普段FWがよしなにやってくれている部分を勉強するいい機会になりました。
参考にさせていただいた資料はとてもよくまとまっているので、併せて参照していただければと思います!