このブログははてなブログからの移行記事です。

文字参照とは

基礎的な話だけどきちんと調べたことなかったので適当に調べてみた。

文字参照とはHTML等のマークアップ文書において直接参照できない文字(例えば文章中に<を入れるとタグが崩れちゃったりする)を表現するために用いられる文字列です。 PHPだとhtmlspecialchars()を一度は使用すると思うのですが、その時に出力される文字達が文字参照です。

この文字参照には以下の二種類あります。

それぞれ説明します。

数値文字参照(文字参照)

数値文字参照は特定の文字を10進数、もしくは16進数によって指定する方法です。 例えばを10進数の数値文字参照で表した文字列です。

文字実体参照(実体参照)

こちらは数値文字参照と違い、特定のキーワード文字列でHTML等に使用される文字集合の該当文字列を表現する方法です。 代表的なものだと<, >, &, "等があります。 以下のような文章を打ちたいときには文字実体参照を使うことで実現できます。

<html>
  <head>
    <title>sample</title>
  </head>
  <body>
    <!-- これだとブラウザによってはタグが破壊される -->
    <p><title>タグはページのタイトルをつけるのに用います</p>
    <!-- こう書けば見た目上は<title>になる -->
    <p><title>タグはページのタイトルをつけるのに用います</p>
  </body>
</html>

知っとくと面白いイベントハンドラ属性における実体参照

そもそもこの話を調べることになったキッカケがHTML内にhtmlspecialchars()を使わずにサニタイズしたURLを出力したかったからです。 その際にセキュリティ最強マンの友人にいろいろ聞いた過程で出てきた話がイベントハンドラ属性における実体参照の話です。

そもそもイベントハンドラ属性とは、特定のHTMLタグにつけることのできる特定のイベント時の処理を書くための属性値です。 具体的には以下の様なものがイベントハンドラ属性値。

<button onclick="alert('ボタンがクリックされたよ!');">
<img src="x" onerror="alert('画像読み込みエラーだよ!');">

これはしばしばXSSの温床になる属性だったりするのですが、このイベントハンドラ内での実体参照の解釈がすこし特殊。

例えば以下の様なPHPのエスケープは有効です。

<p class="<?php echo htmlspecialchar($className, ENT_QUOTES); ?>">ほげ~</p>

このようにエスケープすれば通常は$className"onclick="alert('xss');みたいな文字列を入れられても出力時点で実体参照に置き換えられるので問題はないです。 じゃあ以下のような時はどうなるでしょう。

<button onclick="alert('<?php echo htmlspecialchars($onClickMessage, ENT_QUOTES); ?>');">押せよオラオラ</button>

この際、$onClickMessage');location.href='http://evil.com';alert('のような文字列を混ぜます。 すると出力されるのは以下のHTMLです。

<button onclick="alert('');location.href='http://evil.com';alert('');">押せよオラオラ</button>

数値文字参照に置き換えられて、一見問題がないように思えます。 しかしブラウザがイベントハンドラ属性の値を評価する際、文字参照を解釈した上でコードの実行を試みます。 つまり見た目上はエスケープされていても実際に攻撃が成功します。

<!-- 「ソースを読む」で見たらこうなってるのに -->
<button onclick="alert('');location.href='http://evil.com';alert('');">押せよオラオラ</button>
<!-- ブラウザの解釈はこう -->
<button onclick="alert('');location.href='http://evil.com';alert('');">押せよオラオラ</button>

コワイですね。

どうすればいいの

この記事に全て書いてあります。

対策遅らせるHTMLエンコーディングの「神話」

1行でまとめるなら「全ての文字列をHTMLエスケープしよう」です。 XSS対策する上で一番の敵はブラウザの仕様やコードでなく、人です。 「危なそうなところはエスケープ」といった運用ではヒューマンエラーでうっかりエスケープ忘れ、なんてことが必ず起こります。 半年くらいなら起きないかもしれませんが、10年開発して0件なんてことはないですよね…? 文字列は必ずエスケープしましょう。 どうしてもエスケープできない場面はエスケープする実装方法に差し替えられないか検討し、それでもダメならセキュリティできる人、ないしはチームメンバーに一言相談するのがマストだと思います。