XSS対策:JavaScriptなどのエスケープ

昨日の日記に対して、id:ikepyonさんからトラックバックを頂戴した。内容はそちら(Tipsと考え方とXSS対策)を読んでいただくとして、興味深いテーマなので少し突っ込んでみたい。

# 日によって「です・ます」で書いたり、「だ・である」で書いているのは気分の問題なので、あまり気にしないでいただきたい

Tipsだけでなく、物事の本質を見極め、何が危険で、何が安全なのかということを考える必要があると思う。

昨日の記事は、(一般的な)XSS対策として、どの文字をエスケープするのが「本質的」だったかを考えたかったのであって、あれをTipsととらえると確かに失敗する。

JavaScriptスクリプトなどが入っている場合も昨日と同じ方法論で考えることは可能である。まずはこれを検討してみよう。

スクリプトがonXXXのイベントハンドラとして記述されている場合

この場合は、HTMLタグの属性値としてスクリプトが記述されるので、昨日示した●●●●部分=スクリプト文字参照によるエスケープが必要となる。

"●●●●●●●●●●●●"

具体的には、(最低)「"」と「&」である。

<input type="text" onmouseover="alert('Hello world');">

こう書くほうが普通だが、書こうと思えば、

<input type="text" onmouseover="alert(&quot;Hello world&quot;);">

こう書いても良い。
しかし、スクリプト内も文字列リテラルのようにエスケープが必要になる箇所がある。以下のように。

<input type="text" onmouseover="alert('Hello JavaScript\'s world');">

さらに、JavaScript文字列リテラルエスケープと文字参照を組み合わせなければならないケースも出てくる。

<input type="text" onmouseover="alert(&quot;He said \&quot;Hello world\&quot;&quot;);">

だんだん、訳がわからなくなってくる(^^;
しかし、原理は簡単である。

"●●●●●'▲▲▲▲'●●●●●"

上記のように、入れ子構造になっている場合には、、

  • ▲の部分は内側のエスケープを行う
  • エスケープ済みの▲を含めて、●全体のエスケープを行う

これでよいのだが、どちらかを怠るとXSS脆弱性が発生する。
たとえば、以下のようなスクリプト(▲▲▲は入力データ)があったとする。

<input type="text" onmouseover="alert('入力は ▲▲▲ です');">

ここで、

▲▲▲ = "');alert(document.cookie);// "

を入力すると、スクリプト全体では以下のようになる(見やすいように改行を追加した)。

<input type="text" onmouseover="alert('入力は ');
alert(document.domain);// です');">

すなわち、別のスクリプトを挿入することが出来る。しかも、上記に出てくるシングルクォートを文字参照(&#39;)に変換してもXSS対策にはならない。JavaScriptの文字列リテラルのルールに従ってエスケープしなければならないのである。

スクリプトがSCRIPTタグにより記述されている場合

この場合は、以下のような構造となる(●●●がスクリプト)。

<SCRIPT type="text/javascript">●●●●</SCRIPT>

したがって、JavaScriptとしてのエスケープを実施したうえで、スクリプトの終端に対するエスケープを実施する必要がある。しかし、スクリプト要素内では文字参照によるエスケープは出来ないので、終端文字列を別の文字列に置き換える代替処理をとる。

W3Cによると、スクリプトの終端は、</[a-zA-Z]である。すなわち、</の後に英字が続いた時点でスクリプトの終了なのだが、IE/Firefox/Operaの主要ブラウザで実験したところ、</SCRIPT>でなければ終端とみなさなかった。
スクリプトタグの要素中では、JavaScriptの文法は関知しないので、たとえ文字列リテラル中に</SCRIPT>があっても、その時点でスクリプトの終了とみなされる。この性質を利用したXSSが可能である。

以下のようなスクリプトがあっとする(▲▲▲は入力データ)。

<script>alert("入力データは▲▲▲です");</script>

ここで、

▲▲▲ = "</script><script>alert(document.cookie);//"

を入力すると、スクリプト全体では以下のようになる(見やすいように改行を追加した)。

<script>alert("入力データは</script> // 構文エラーで実行されない
<script>alert(document.cookie);//です");</script> // 実行される

この場合、たとえダブルクォートのJavaScriptとしてのエスケープを実施していてもXSSは成立する。通常これを意識しない場合が多いので注意が必要である。

対策としては、W3CのHTML4.01仕様 B.3.2にあるように、文字列リテラルに</[a-zA-Z]に相当する文字列が入る場合は、<\/[a-zA-Z]に変換してやることである(前述のようにこれはHTMLのエスケープではない)。ここで、現実のブラウザはスクリプトの終端として</SCRIPT>のみを認識するとしても、対策は安全サイドで判断して、より広範囲であるW3Cの規約に準拠しておくことが大切である。

現実にはどうすればよいのか

ikepyonさんの問題提起に乗っかる形で、本質的にはどうすべきなのかを説明してきたが、既に見てきたように、入れ子エスケープをきちんと理解してバグなく実装するのはかなりやっかいである。
安全なWebサイトの作り方改定第2版には、

<script>...</script> 要素の内容を動的に生成しないようにする

という注意はあるが、イベントハンドラに関する注意はない。僕としては、いずれの場合にもスクリプトの内容を動的生成するのを避けるというガイドラインを推奨したい。
具体的には、パラメータ部分をHTMLのhidden fieldとして定義して、スクリプトから参照する形をとればよい。そうすれば、スクリプトは固定にしたうえで、動的なパラメータを与えることが出来る。もちろん、hiddenフィールドの属性値のエスケープをお忘れなく。

2007/05/14 19:57 タイトル修正