JavaScriptセキュリティの基礎知識

第7回DOM-based XSS その2

前回は、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]⁠。

// bad code
document.write(location.referrer);

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へ出力する前にエスケープしています。

function htmlEscape(s) {
    s = s.replace(/&/g, "&")
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/"/g, """)
        .replace(/'/g, "'");
    return s;
}

// 変数text,urlはどちらも攻撃者がコントロール可能な文字列
var html = '<div>' + htmlEscape(text) + '<img src="' + htmlEscape(url) + '" alt="' + htmlEscape(text) + '"></div>';
document.write(html);

当然ながら、従来のサーバ側でのXSS対策と同様、document.writeの呼び出し中で1か所でもエスケープの漏れがあると、DOM-based XSSが発生してしまいます。

繰り返しになりますが、DOMへ文字列や要素を追加するのであれば、document.writeを使用するのではなく、DOM操作APIを利用することを推奨します。

eval~現在のブラウザならJSON.parseを利用する

evalは、引数として与えられた文字列を式として評価、あるいはJavaScriptのステートメントとして実行します。ですので、evalの引数に攻撃者がコントロール可能な文字列を渡した場合には、攻撃者が自由にJavaScriptを実行できてしまいます。

もしかすると、古い資料などには、JSON文字列をJavaScriptのオブジェクトに変換するためにevalを利用した以下のようなコード例を載せているかもしれません。

// bad code
var json = '{ "name" : "hasegawa", "url" : "http://utf-8.jp/" }';
var obj = eval( "(" + json + ")" );

しかし、このようなJSON文字列からオブジェクトを生成する場合、現在のブラウザではJSON.parseが利用できるので、evalを呼び出す必要はありません。

IE7のように、JSON.parseがサポートされていない古いブラウザをどうしてもサポートしなければいけない場合には、json2.js(https://github.com/douglascrockford/JSON-js/blob/master/json2.js)を読み込むことで同様の機能を利用できます。

if (window.JSON === undefined) {
    var elm = document.createElement("script");
    elm.setAttribute("type", "text/javascript");
    elm.setAttribute("src", "json2.js");
    document.body.appendChild(elm);
}

/* ... */

var json = '{ "name" : "hasegawa", "url" : "http://utf-8.jp/" }';
var obj = JSON.parse(json );

そもそも、ほとんどの一般的なプログラムではevalを使う必要性はないでしょう。どうしてもevalを使用しなければならない場合には、引数として攻撃者がコントロール可能な文字列が渡らないようにしましょう。

// 変数textは攻撃者がコントロール可能な文字列
function foo (text) {
    var codes = {
        "alert" : "alert('Hello from alert');",
        "prompt" : "prompt('Enter the tex')",
        "confirm" : "confirm('Choose ok or cancel');"
    };
    if (codes[text] !== undefined) {
        eval(codes[text]); // evalには攻撃者がコントロール可能な文字列は渡らない
    }
}

setTimeout/setInterval~引数では文字列ではなく関数を渡すようにする

setTimeoutやsetIntervalは、引数で与えられた関数を一定時間後に(setIntervalは一定間隔で繰り返し)実行します。このとき、第1引数に関数の代わりに文字列を与えた場合には、evalと同様、その文字列をJavaScriptとして実行します。以下のようなコードでは、攻撃者が変数textを自由にコントロールできた場合にはsetTimeoutを経由して攻撃者が自由にJavaScriptを実行できてしまいます。

// bad code
// 変数 text は攻撃者がコントロール可能な文字列
setTimeout("alert('" + text + "');", 1000);

そのため、以下のように、setTimeoutやsetIntervalには文字列ではなく関数を渡すようにしましょう。

// 変数tex は攻撃者がコントロール可能な文字列
setTimeout(function( s ){ alert( s ); }, 1000, text);

なお、このコードではsetTimeoutに追加の引数を与えることでコールバック関数にその引数を渡していますが、この機能はIE9では使用できません。もしIE9もサポート対象として含めるのであれば、次のようなクロージャを利用したコードを書くといいいでしょう[2]⁠。

// 変数textは攻撃者がコントロール可能な文字列
setTimeout( 
    (function (s){ 
        return function(){ alert(s); }; 
    })(text), 1000 
);

Function~引数にコントロール可能な文字列が渡らないようにする

Functionコンストラクタを使用すると、文字列からFunctionオブジェクトを生成し、それを呼び出すことができます。以下のようなコードでは、攻撃者が変数textを自由にコントロールできた場合、Functionコンストラクタを通じて攻撃者が自由にJavaScriptを実行できてしまいます。

// bad code
// 変数textは攻撃者がコントロール可能な文字列
var func = new Function("alert('" + text + "'); return 0;");
func();

そもそも、ほとんどの一般的なプログラムではFunctionコンストラクタを用いて動的にコードを生成する必要性はないでしょう。どうしてもFunctionコンストラクタを使用しなければならない場合には、引数として攻撃者がコントロール可能な文字列が渡らないようにしましょう。

jQuery()/$()/$.html()~自分で書くときより挙動が見えにくくなるのでいっそう注意を

jQueryは、以前ほどではありませんが今でも広く使用されているJavaScriptライブラリであり、DOMの操作においてもJavaScriptそのままで操作を行うのに比べ便利なAPIを多数提供しています。そういったjQueryの便利なAPIにもDOM-based XSSのシンクとして働く機能が多くあるので、注意が必要です。

jQueryのAPIでシンクとして働く代表的な機能としては、jQuery()、$()、$.html()などがあります。これらは、いずれも攻撃者が自由に引数を渡せた場合にはHTML要素を生成し、DOM-baed XSSが発生することになります。

// bad code
// 変数textは攻撃者がコントロール可能な文字列
$("#element").html(text); // 通常のinnerHTMLと同様に任意のHTMLが生成される
$(text).append("<div>news</div>"); // textに「<img src=# onerror='alert(1)'>」などが設定されるとスクリプトとして実行される

jQueryを使う場合にDOM-based XSSを避けるには、以下のような配慮が必要となります。

  • $.html()ではなく、$.text()を使う
  • $()へ渡すセレクタは、攻撃者がコントロールできないようにする

jQueryに限った話ではありませんが、ライブラリを使っている場合には、自身で生のJavaScriptを書いている場合に比べ、それぞれのAPIがDOM-based XSSのシンクとして機能してしまうのかどうかが見えにくくなります。呼び出すAPIがシンクとして機能することがないかの確認や、シンクとして機能するAPIを呼び出す場合には引数となる文字列を攻撃者がコントロールできないようにするといった注意が必要になります。

今回は、実際にDOM-based XSSの原因として見かける代表的なシンクを紹介しました。

次回は、実際のJavaScriptプログラミングにおいて遭遇するさまざまなシチュエーションにおける、より実践的なDOM-based XSS対策について説明します。

おすすめ記事

記事・ニュース一覧