金利0無利息キャッシング – キャッシングできます

 | 

2010-02-22

HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について

21:51 | HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について - 金利0無利息キャッシング – キャッシングできます を含むブックマーク はてなブックマーク - HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について - 金利0無利息キャッシング – キャッシングできます

あとで書く → 書いた

概要

主にHTMLを生成することを主目的として書かれているテンプレートエンジンを使って、JavaScriptのコードを生成するようなケースにおいて、エスケープ方式の選択ミスに起因する脆弱性がとても多い。ので傾向と対策を述べる。サンプルコードはPerlとTT(Template Toolkit)で書いているが、他の言語でも同等の問題が発生する可能性がある。

具体的な事例としては

  • ユーザーが検索した文字列をJavaScriptの変数として埋め込んでいるケース
    • Google Adsenseなどの広告表示のチューニング用のパラメータ
    • Google Mapsなどの外部ライブラリへのパラメータの埋込み
    • アクセス解析や何らかの外部検索APIへのクエリパラメータ
    • Flashのタグ生成のパラメータ
  • などなど。

scriptタグ中に文字列を埋め込む

こういうコードでエスケープしていると仮定する。(注:最近のテンプレートエンジンはシングルクオートもエスケープすることが多いです)

sub escape_html {
	my $str = shift;
	$str =~s/&/&/g;
	$str =~s/</&lt;/g;
	$str =~s/>/&gt;/g;
	$str =~s/"/&quot;/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>

何がマズイのかというと

  • 1のケースはHTMLエスケープでシングルクオートがエスケープされていないので、'+alert(1)+'などで文字列リテラルの途中にコードを挿入できる。
  • 2や3のケースは「\' → \\'」となってクオートのエスケープを無効化することが出来る
  • 4のケースは、文字列末尾に \ を加えることで閉じるクオートをエスケープして無効化 → スクリプトエラーを引き起こせる(ただしこれが危険な動作になる可能性は低いだろう)
    • 埋め込んだ文字列がdocument.writeやinnerHTMLで出力されるのであれば、HTMLエスケープ済みでも問題が起きる。
HTMLエスケープでは不完全なサンプル
<script>
 document.write("[% value | html %]");
</script>

このHTMLエスケープ関数で出力される文字列は

  • HTMLエスケープされていてダブルクオートが含まれないことが保証されている。
  • 末尾に\を入れることでエラーを起こすことはできるが、コードは挿入できない。
  • 埋め込まれるvalueは「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するという正しいコードになる。

scriptタグ中にJSONを埋め込む

JSONはデータ構造を完全にvalidなjsのコードに変換してくれる。余計なことを考えなくてよい。script中に複雑なデータ構造を埋め込みたいのであればJSONを使うべきだ。

<script>
var hash = [% hash | json %]; // あるいは [% hash.to_json %] 的な何か
</script>

これはプログラマ的にとても正しいコードに見えるのだが、HTMLに埋めこむことを考えると危険である。なぜかというと、JSON(String, Array, Hashいずれでも)のvalueの箇所に </script> が含まれていた場合には

  • scriptタグが終了する → HTMLエスケープされていない → 自由にタグが記述できる

という状況が発生するからだ。なので、scriptタグ中にJavaScriptの変数を出力する場合は、JSONに加えて「</script>が含まれない」という条件を満たす必要がある。

回避するためには、下記のいずれかの方法を取る必要がある。

  • テンプレートエンジンでJSONを生成する(多くの場合間違えるので、推奨しない)
  • scriptタグの中でJSONを使わない
  • 可能であればJSONライブラリのオプションで<>/いずれかをエスケープする。
  • 生成されたJSON文字列の<>/いずれかを正規表現などを使って置換する。
  • JSONのvalueに当たる部分には「HTMLエスケープ済みの文字列を入れる」という規約を設けて事前にエスケープする。

RubyJSONライブラリの古いバージョン(確認したところ1.1.4)では / が \/ に置換されていてこの問題が起こらない。これは意図した挙動なのかと思ったのだが、最新版にアップデートしたらそうではなくなっていた、単なるバグだったようだ。デフォルトで/をエスケープするのは、JSの文字列リテラルとしては "/" と "\/" は等価になるので、まあ問題ない挙動ではあるが、/が現れるたびに1byte余計に増えるだとか、JSONをデコードしないで正規表現でURLを抜き出したいだとか、そういう用途で困ることになるだろう。個人的には「HTMLに埋め込んでも安全なJSON」というのはJSONの仕様の外であるので、JSONライブラリが気を使う必要はないと思う(そういう要望に答えられるようなオプションがあれば良いとは思う)

jsファイルを動的に生成するようなケースでは、JSONは完全にvalidなコードを生成する。HTMLscriptタグ中に出力されるのであれば、それはHTMLの一部でもあるということを意識しないといけない。

エスケープ方式を間違えても安全にするにはどうするか

サービスに脆弱性が発見された場合、自作ツールやackgrepを組み合わせて、エスケープされていないテンプレート変数を網羅的に見つけるようにしている。しかし、scriptタグ、onclickなどのjsの変数が埋め込まれている箇所でHTMLエスケープが使われているケースでは「エスケープ済みだが危険」という状況になる。これは発見が困難で、見落としやすい。文脈を判断した上で、適切なエスケープがなされているのかを(主に人間が)判別してやる必要がある。これはとても面倒くさい。

(人間はミスをするので)間違ったエスケープ方式を選択したとしても、脆弱性が発生しづらい状況を作る必要がある。

  • 本来jsエスケープすべき箇所をHTMLエスケープのみ適用している場合
  • HTMLエスケープ済みの文字列がjsとして評価されても安全なようにする

逆のケースも想定すると

  • 本来HTMLエスケープすべき箇所をjsエスケープのみ適用している場合
  • jsエスケープ済みの文字列はHTMLとして評価されても安全なようにする

ここで、HTMLエスケープに\を&#92;に置換するルールを加える。すると「本来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>
  • 1. エスケープ無し、任意の値が入るなら当然まずい。自動エスケープでも2と同様に問題有り。
  • 2. HTMLの実体参照や数値参照が解釈された上で、jsが実行されるので任意のタグを出力可能。シングルクオートを閉じて任意のコード挿入可能
  • 3. HTMLエスケープされていないので(別途valueにバリデーションがあったりしなければ)任意のタグを出力可能、jsが過剰エスケープであってもvalueを事前にHTMLエスケープしてないなら任意タグが出力可能
  • 4. (jsエスケープで&が数値参照になっていないなら)HTMLの実体参照や数値参照が解釈された上で、jsが実行されるので任意のタグを出力可能。
    • jsエスケープが過剰エスケープであるならば(HTMLの実体参照や数値参照が機能しなくなるので)安全。
    • jsエスケープが「HTMLエスケープ済み」フラグを解除して、さらに自動でHTMLエスケープされるのであれば安全。
  • 5. \がエスケープされないならJSの数値参照でタグを出力可能、テンプレートエンジンが型を持っていれば二重エスケープされない可能性がある。
  • 6. 正しい(ただしテンプレートエンジンに型がある場合はエスケープ済みの変数を再度エスケープしないかも知れない)

このケースは「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を閉じる」が可能だし、過剰エスケープでないなら「&apos;でシングルクオートを閉じる」が可能、過剰エスケープの場合でも(画面出力に関わる処理がないか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のみを過剰エスケープに変更した場合 < と > が \x3c \x3e に置換済みになるため「HTMLエスケープが機能しなくなり」新規にXSSが発生する。
  • js, html両方が過剰エスケープの場合、jsの数値参照が解釈されなくなり、意図した出力結果が得られない(が、脆弱性にはならない)
    • jsエスケープがどのように文字列を置換するのかを表示する意図であれば正しい(HTMLエスケープが\をエスケープしないなら危険)

既存のテンプレートを全て網羅的にスキャンするのが困難である場合は、エスケープルールを変えることで対応すると楽なわけだが、jsのみを過剰エスケープにした場合「適用順序を間違えているが、たまたま問題なく動いているコード」を破壊する可能性があることに注意する必要がある。画面出力に使われる変数の場合は、新規にXSSが発生することになる。適用順序を間違えている場合、そもそも正しく動くことが期待できないわけだが、新規にXSSが発生することを防ぐためにはエスケープルールを見直す前に、HTMLエスケープ以外の既存のエスケープルールの適用箇所をチェックしなければならない。

JavaScript側におけるアプローチ

  • サーバーが出力した文字列を、特に加工せずにそのまま出力するケース
  • 文字列ベースのテンプレートエンジンを使っていて、innerHTMLに代入するケース

という場合には、サーバー側であらかじめHTMLエスケープされていたほうが都合が良い。

逆に

  • もっぱらデータとして生の値を必要とするケース
  • DOMベースのテンプレートエンジンを使うケース

であれば、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エスケープ済みの文字列を返す」と仕様に書かれていたとしても、それが確実にエスケープされているという信用がないので

  • 自社のサーバー側でproxyして、unescapeした上でescapeする。
  • もしくはJavaScript側でunescapeしたうえでescapeする

というアプローチが必要になるだろう(相手サービスがどの程度信用できるのかにもよる)

hidden要素から拾ってくるのはどうなのか

社内向けには「すでに画面に出力されているなら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のアプローチのほうが安全かつコードが短くなる。問題点としては、何もしないコードが実際は安全であること(意図的なのかエスケープ漏れなのか分からない)、テンプレートエンジンやエスケープルールを変えた場合には安全ではなくなること。

まとめ

  • 自動エスケープされていてもXSSが発生することはある。
  • scriptタグの中に何も考えないでJSON入れるのは危険。
  • 正しい方式でエスケープされているのかを機械的に判断するのはとても面倒くさい。コードを読まないといけない。
  • 変数の型と評価されるコンテキストを理解して、正しいエスケープ方式を選択するのが肝心である。
  • が、人間はミスをするのでセキュアコーディングの観点から見ると「いずれかの方法で最低1回エスケープされていれば」XSSを防ぐことが出来るのが楽である。

参考文献

ウェブアプリケーションセキュリティ[ハードカバーで読みづらい]

http://www.amazon.co.jp/dp/4887189400

s-askas-aska2010/11/13 11:55hidden要素のvalue属性に値を入れて取り出すという方法だと安全という事はないでしょうか、いかがでしょうか。

malamala2010/11/13 13:25js側で生の値が必要な場合はそうです。が、js側でエスケープ済みかどうかを意識する必要があります。画面出力したりevalしたりするのであれば安全ではないです。本文に追記しました。

 |