前回は、DOM-based XSSの原因と対策についての概略を解説し、DOM-based XSSを引き起こすシンクの事例としてinnerHTMLへのHTMLの代入やlocationオブジェクトへのURLの代入をとりあげました。今回は、innerHTMLやlocationオブジェクトほど頻繁ではないものの、実際にDOM-based XSSの原因として見かけるシンクの代表的なものについて説明します。
document.write/document.writeln~できるだけ使わず、代替手段を利用する
DOMのレンダリングを遅延させるなどの理由から、以前に比べるとdocument.writeの使用される頻度は減っていますが、それでもなお広告用のJavaScriptなど一部では根強くdocument.writeが使われています。document.writeやdocument.writelnでは、引数に攻撃者がコントロール可能な文字列が渡された場合にはDOM-based XSSが発生します。
たとえば、以下のようなコードでは、攻撃者が自身のサイトにてhttp://attacker.example.com/?<script>alert(1)</script>のようなページを作り、そこから攻撃対象のサイトへ移動させることで、リファラを経由してXSSを発生させることができます[1]。
document.writeによるDOM-based XSSの発生を抑える最善の方法は、document.writeを使用しないことです。以下のように、代替となる手段を用いることで、document.writeを使用せずに同様の目的を達成することができます。
- HTML中に単純に文字列を出力したいのであれば、前回解説したようにdocument.createTextNodeを用いてテキストノードを生成する
- HTMLを生成するのであれば、document.createElementを用いてDOM操作を行う
どうしてもdocument.writeを使わなければいけない場合には、「document.writeで出力するコンテキストに応じて文字列をエスケープする」という、これまでサーバサイドで行っていたXSS対策と同じことをJavaScript上で行う必要があります。すなわち、テキストノードやHTML要素の属性値に対してdocument.writeを使う場合には、「<」「>」「"」「'」「&」の各文字をエスケープして出力する、ということになります。
以下のコードでは、従来サーバ側で行っていたXSS対策と同様に、攻撃者がコントロール可能な変数であるtextおよびurlをdocument.writeへ出力する前にエスケープしています。
当然ながら、従来のサーバ側でのXSS対策と同様、document.writeの呼び出し中で1か所でもエスケープの漏れがあると、DOM-based XSSが発生してしまいます。
繰り返しになりますが、DOMへ文字列や要素を追加するのであれば、document.writeを使用するのではなく、DOM操作APIを利用することを推奨します。
eval~現在のブラウザならJSON.parseを利用する
evalは、引数として与えられた文字列を式として評価、あるいはJavaScriptのステートメントとして実行します。ですので、evalの引数に攻撃者がコントロール可能な文字列を渡した場合には、攻撃者が自由にJavaScriptを実行できてしまいます。
もしかすると、古い資料などには、JSON文字列をJavaScriptのオブジェクトに変換するためにevalを利用した以下のようなコード例を載せているかもしれません。
しかし、このようなJSON文字列からオブジェクトを生成する場合、現在のブラウザではJSON.parseが利用できるので、evalを呼び出す必要はありません。
IE7のように、JSON.parseがサポートされていない古いブラウザをどうしてもサポートしなければいけない場合には、json2.js(https://github.com/douglascrockford/JSON-js/blob/master/json2.js)を読み込むことで同様の機能を利用できます。
そもそも、ほとんどの一般的なプログラムではevalを使う必要性はないでしょう。どうしてもevalを使用しなければならない場合には、引数として攻撃者がコントロール可能な文字列が渡らないようにしましょう。
setTimeout/setInterval~引数では文字列ではなく関数を渡すようにする
setTimeoutやsetIntervalは、引数で与えられた関数を一定時間後に(setIntervalは一定間隔で繰り返し)実行します。このとき、第1引数に関数の代わりに文字列を与えた場合には、evalと同様、その文字列をJavaScriptとして実行します。以下のようなコードでは、攻撃者が変数textを自由にコントロールできた場合にはsetTimeoutを経由して攻撃者が自由にJavaScriptを実行できてしまいます。
そのため、以下のように、setTimeoutやsetIntervalには文字列ではなく関数を渡すようにしましょう。
なお、このコードではsetTimeoutに追加の引数を与えることでコールバック関数にその引数を渡していますが、この機能はIE9では使用できません。もしIE9もサポート対象として含めるのであれば、次のようなクロージャを利用したコードを書くといいいでしょう[2]。
Function~引数にコントロール可能な文字列が渡らないようにする
Functionコンストラクタを使用すると、文字列からFunctionオブジェクトを生成し、それを呼び出すことができます。以下のようなコードでは、攻撃者が変数textを自由にコントロールできた場合、Functionコンストラクタを通じて攻撃者が自由にJavaScriptを実行できてしまいます。
そもそも、ほとんどの一般的なプログラムではFunctionコンストラクタを用いて動的にコードを生成する必要性はないでしょう。どうしてもFunctionコンストラクタを使用しなければならない場合には、引数として攻撃者がコントロール可能な文字列が渡らないようにしましょう。
jQuery()/$()/$.html()~自分で書くときより挙動が見えにくくなるのでいっそう注意を
jQueryは、以前ほどではありませんが今でも広く使用されているJavaScriptライブラリであり、DOMの操作においてもJavaScriptそのままで操作を行うのに比べ便利なAPIを多数提供しています。そういったjQueryの便利なAPIにもDOM-based XSSのシンクとして働く機能が多くあるので、注意が必要です。
jQueryのAPIでシンクとして働く代表的な機能としては、jQuery()、$()、$.html()などがあります。これらは、いずれも攻撃者が自由に引数を渡せた場合にはHTML要素を生成し、DOM-baed XSSが発生することになります。
jQueryを使う場合にDOM-based XSSを避けるには、以下のような配慮が必要となります。
- $.html()ではなく、$.text()を使う
- $()へ渡すセレクタは、攻撃者がコントロールできないようにする
jQueryに限った話ではありませんが、ライブラリを使っている場合には、自身で生のJavaScriptを書いている場合に比べ、それぞれのAPIがDOM-based XSSのシンクとして機能してしまうのかどうかが見えにくくなります。呼び出すAPIがシンクとして機能することがないかの確認や、シンクとして機能するAPIを呼び出す場合には引数となる文字列を攻撃者がコントロールできないようにするといった注意が必要になります。
今回は、実際にDOM-based XSSの原因として見かける代表的なシンクを紹介しました。
次回は、実際のJavaScriptプログラミングにおいて遭遇するさまざまなシチュエーションにおける、より実践的なDOM-based XSS対策について説明します。