![]() ![]() ![]() ![]() |
![]() |
|
![]() |
||
![]() |
あとで書く → 書いた
主にHTMLを生成することを主目的として書かれているテンプレートエンジンを使って、JavaScriptのコードを生成するようなケースにおいて、エスケープ方式の選択ミスに起因する脆弱性がとても多い。ので傾向と対策を述べる。サンプルコードはPerlとTT(Template Toolkit)で書いているが、他の言語でも同等の問題が発生する可能性がある。
具体的な事例としては
こういうコードでエスケープしていると仮定する。(注:最近のテンプレートエンジンはシングルクオートもエスケープすることが多いです)
sub escape_html { my $str = shift; $str =~s/&/&/g; $str =~s/</</g; $str =~s/>/>/g; $str =~s/"/"/g; $str; } sub escape_js { my $str = shift; $str =~s/"/\"/; $str =~s/'/\'/; $str; }
このようなエスケープルールを使っている場合、いずれの場合でも問題が起きる。
<script> var value = '[% value | html %]'; // 1 var value = '[% value | js %]'; // 2 var value = '[% value | html | js %]'; // 3 var value = "[% value | html %]"; // 4 </script>
何がマズイのかというと
<script> document.write("[% value | html %]"); </script>
このHTMLエスケープ関数で出力される文字列は
JSとして評価された際に「文法エラーやコードのインジェクションが起こらない」という状況であっても、document.writeの結果としてコードをインジェクションすることが出来る、という状況が起こりうる。具体的にはバックスラッシュがそのまま通るので、JavaScriptの文字列リテラルの数値参照を使ってタグを含めることが出来る。
具体例としては出力結果がこのようになるケース
<script> document.write("\x3cscript\x3ealert(1)\x3c/script\x3e"); </script>
HTMLエスケープが自動化されているテンプレートエンジンを使っている場合でも(過剰なエスケープをしていないのであれば)この問題は発生する。多くのテンプレートエンジンのHTMLエスケープ処理は\をエスケープしない。
正しいコードは
<script> document.write("[% value | html | js %]"); </script>
かつ、jsエスケープが\を\\に置換している必要がある。こうすることで「HTMLエスケープ済みの文字列」をJavaScriptの変数として埋め込んで、それをdocument.writeするという正しいコードになる。
JSONはデータ構造を完全にvalidなjsのコードに変換してくれる。余計なことを考えなくてよい。script中に複雑なデータ構造を埋め込みたいのであればJSONを使うべきだ。
<script> var hash = [% hash | json %]; // あるいは [% hash.to_json %] 的な何か </script>
これはプログラマ的にとても正しいコードに見えるのだが、HTMLに埋めこむことを考えると危険である。なぜかというと、JSON(String, Array, Hashいずれでも)のvalueの箇所に </script> が含まれていた場合には
という状況が発生するからだ。なので、scriptタグ中にJavaScriptの変数を出力する場合は、JSONに加えて「</script>が含まれない」という条件を満たす必要がある。
回避するためには、下記のいずれかの方法を取る必要がある。
RubyのJSONライブラリの古いバージョン(確認したところ1.1.4)では / が \/ に置換されていてこの問題が起こらない。これは意図した挙動なのかと思ったのだが、最新版にアップデートしたらそうではなくなっていた、単なるバグだったようだ。デフォルトで/をエスケープするのは、JSの文字列リテラルとしては "/" と "\/" は等価になるので、まあ問題ない挙動ではあるが、/が現れるたびに1byte余計に増えるだとか、JSONをデコードしないで正規表現でURLを抜き出したいだとか、そういう用途で困ることになるだろう。個人的には「HTMLに埋め込んでも安全なJSON」というのはJSONの仕様の外であるので、JSONライブラリが気を使う必要はないと思う(そういう要望に答えられるようなオプションがあれば良いとは思う)
jsファイルを動的に生成するようなケースでは、JSONは完全にvalidなコードを生成する。HTMLのscriptタグ中に出力されるのであれば、それはHTMLの一部でもあるということを意識しないといけない。
サービスに脆弱性が発見された場合、自作ツールやackやgrepを組み合わせて、エスケープされていないテンプレート変数を網羅的に見つけるようにしている。しかし、scriptタグ、onclickなどのjsの変数が埋め込まれている箇所でHTMLエスケープが使われているケースでは「エスケープ済みだが危険」という状況になる。これは発見が困難で、見落としやすい。文脈を判断した上で、適切なエスケープがなされているのかを(主に人間が)判別してやる必要がある。これはとても面倒くさい。
(人間はミスをするので)間違ったエスケープ方式を選択したとしても、脆弱性が発生しづらい状況を作る必要がある。
逆のケースも想定すると
ここで、HTMLエスケープに\を\に置換するルールを加える。すると「本来JSエスケープする箇所でHTMLエスケープしか適用されていないケース」でテンプレート変数をdocument.writeするコードが書かれていたとしても、文字列中の数値参照が展開されないのでタグが有効にならない。(例外的にXHTMLで、CDATAを使わずにscriptを動的に出力するケースでは「HTMLの実体参照、数値参照が解釈された上で」scriptが実行されるので、過剰なエスケープでは対策にならない)
さらに、jsエスケープでは'"<>&を、それぞれ数値参照にしてやる。こうすることで、「本来HTMLエスケープすべき箇所でJSエスケープしか適用されていないケース」においても、タグを無効化することが出来る。参考 https://gist.github.com/672234
参考文献に「過剰なエスケープ」というアプローチが書かれている。これは"'&<>だけでなく、記号全てを数値参照にエスケープすることで、異なるコンテキストで評価された場合でも危険な挙動を防ぐ事ができる、という考え方だ。
jsエスケープしたものを、さらにHTMLエスケープする必要があるケース。
1. <button onclick="document.getElementById('output').innerHTML='[% value %]'">Hello</button> 2. <button onclick="document.getElementById('output').innerHTML='[% value | html %]'">Hello</button> 3. <button onclick="document.getElementById('output').innerHTML='[% value | js %]'">Hello</button> 4. <button onclick="document.getElementById('output').innerHTML='[% value | html | js %]'">Hello</button> 5. <button onclick="document.getElementById('output').innerHTML='[% value | html | html %]'">Hello</button> 6. <button onclick="document.getElementById('output').innerHTML='[% value | html | js | html %]'">Hello</button>
このケースは「HTMLエスケープ済みの変数をjsとして埋め込み、それをHTMLのattributesの中に記述するのでさらにHTMLエスケープ」というのが正しい。テンプレートエンジンを使ってscriptを生成するのは避けたほうが良いし、attributesの中のコードを動的に生成するのは、さらに避けたほうが良い。
1.のケースに自動で過剰なHTMLエスケープがされていたとしても、HTMLの実体参照や数値参照が解釈された上でjsが実行されるので、任意のタグを出力可能になる。
これはもちろん、とても極端な事例であるけれど、
1. <button onclick="some_function('[% value | html %]')">Hello</button> 2. <button onclick="some_function('[% value | js %]')">Hello</button>
みたいなのは、割とありがちなんじゃないかなあ、と思う。1は(繰り返しになるが)HTMLの実体参照や数値参照が解釈された上でjsが実行されるので、'でjsのコードを破壊できる。2は|jsが " → \" への置換なら「onclickを閉じる」が可能だし、過剰エスケープでないなら「'でシングルクオートを閉じる」が可能、過剰エスケープの場合でも(画面出力に関わる処理がないかsome_functionのコードを追って)js内で必要に応じてHTMLエスケープしてやる必要がある。
これがどうしても必要なケースでの改善案としては、
3. <button onclick="some_function(this)" my_data="[% value %]">Hello</button>
のようにコードは静的+引数に必要なデータは別のattributesに入れる(あるいはtitleなど既存の属性から取ってくる)+自動エスケープするテンプレートエンジンを使う、必要に応じてJavaScript内でHTMLエスケープする。
正しく書かれたコードは、過剰エスケープにしても問題が起こらない。
<script> var value = '[% value | html | js %]'; // HTMLエスケープ済みの文字列をjsとして埋め込む正しいコード document.write(value); </script>
以下は間違えているコード
<script> var value = '[% value | js | html %]'; // 適用順序を間違えている document.write(value); </script> あるいは <script> var value = '[% value | js %]'; // 自動でHTMLエスケープされることを期待している document.write(value); // js側でHTMLエスケープする処理がない </script>
既存のテンプレートを全て網羅的にスキャンするのが困難である場合は、エスケープルールを変えることで対応すると楽なわけだが、jsのみを過剰エスケープにした場合「適用順序を間違えているが、たまたま問題なく動いているコード」を破壊する可能性があることに注意する必要がある。画面出力に使われる変数の場合は、新規にXSSが発生することになる。適用順序を間違えている場合、そもそも正しく動くことが期待できないわけだが、新規にXSSが発生することを防ぐためにはエスケープルールを見直す前に、HTMLエスケープ以外の既存のエスケープルールの適用箇所をチェックしなければならない。
という場合には、サーバー側であらかじめHTMLエスケープされていたほうが都合が良い。
逆に
であれば、JavaScript側でunescapeしてやる必要が出てくるので、HTMLエスケープされていない方が望ましい。document.writeやinnerHTMLの使用を避けて、常にDOMを使って(textNodeへの代入しか行わない)いるのであれば、サーバーサイドでHTMLエスケープされていなくても、JavaScriptによって出力されるHTMLに起因するXSSが発生しない。
ちなみに自分は、文字列テンプレート+innerHTMLというのを好んで使うので、自社サービスに付いてはサーバー側であらかじめエスケープされているJSONを使うのを好む。これはJavaScriptで余計な文字列処理をしなくていいというメリットがある。
JavaScriptに変数を埋め込む場合は、もしJSプログラマが素人で、エスケープ済みかどうかを意識せずに変数をそのままdocument.writeするようなコードを書いていたとしても安全になるように「常にHTMLエスケープ済みの文字列を出力する」のが安全だと考えている。もしtextNodeへの代入や、JavaScript側でもエスケープするコードが書かれていたならば、2重エスケープされることになってカッコ悪いが、それは単なるバグであって脆弱性にはならないからだ。生の値が必要な場合は適宜unescapeしてやればよい。
外部サービスの提供するAPIであれば、例え「HTMLエスケープ済みの文字列を返す」と仕様に書かれていたとしても、それが確実にエスケープされているという信用がないので
というアプローチが必要になるだろう(相手サービスがどの程度信用できるのかにもよる)
社内向けには「すでに画面に出力されているならDOMで拾ってくるのが良い、scriptを動的に生成するのは避けるべき」というふうに言っている。が、DOMから拾ってくる=安全、というわけではない。
<input type="hidden" id="data" value="[% value | html %]"> ← HTMLエスケープされている <script> var value = document.getElementById("data").value; // ← 生の値 document.write(value); // 誤 document.write(escapeHTML(value)); // 正 var t = document.createTextNode(value); // 正 </script>
JavaScriptをちゃんと理解している人がコードを書くなら、まあ間違いは起こらないので杞憂ではあるのだが、「jsをバカが書いても平気なように」するにはhiddenに入るvalueを二重エスケープしておく必要がある。そして、型を持っているテンプレートエンジンでは二重エスケープを抑止する。JavaScript側でHTMLエスケープ済みなのかそうでないのかを意識しなければいけない。
scriptタグ中に変数を埋め込む場合、もっぱら以下の3種類を使い分けることになるわけだが
<script> var value = "[% value %]"; // 1. 自動で過剰HTMLエスケープ、HTMLエスケープ済みのJSに埋め込んでも安全な変数 var value = "[% value | js | raw %]"; // 2. 生の値、HTMLエスケープしない var value = [% value.to_json_in_script_tag | raw %]; // 3. </script>対策されたJSON </script>
JavaScript側でどういう使い方がされるのかをコントロール出来ない(バカが書く)場合は、わざわざhidden要素から拾ってくるよりも、1のアプローチのほうが安全かつコードが短くなる。問題点としては、何もしないコードが実際は安全であること(意図的なのかエスケープ漏れなのか分からない)、テンプレートエンジンやエスケープルールを変えた場合には安全ではなくなること。
ウェブアプリケーションセキュリティ[ハードカバーで読みづらい]
s-aska2010/11/13 11:55hidden要素のvalue属性に値を入れて取り出すという方法だと安全という事はないでしょうか、いかがでしょうか。
mala2010/11/13 13:25js側で生の値が必要な場合はそうです。が、js側でエスケープ済みかどうかを意識する必要があります。画面出力したりevalしたりするのであれば安全ではないです。本文に追記しました。