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

第7回 DOM-based XSS その2

この記事を読むのに必要な時間:およそ 3 分

前回は,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を利用することを推奨します。

※1)
Windows 7や8.1のIEでは,リファラにエンコードされていない「<」「>」を含めることができます。

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 
);
※2)
IE9でsetTimeoutやsetIntervalのコールバック関数に引数を渡すための互換用コードがhttps://developer.mozilla.org/ja/docs/Web/API/window.setTimeoutにあります。また,setTimeoutの呼び出しからalertの実行までの間に変数textが変化しない場合には,必ずしもクロージャは必要ではありません。

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対策について説明します。

著者プロフィール

はせがわようすけ

株式会社セキュアスカイ・テクノロジー常勤技術顧問。 Internet Explorer,Mozilla FirefoxをはじめWebアプリケーションに関する多数の脆弱性を発見。 Black Hat Japan 2008,韓国POC 2008,2010,OWASP AppSec APAC 2014他講演多数。 OWASP Kansai Chapter Leader / OWASP Japan Board member。

URL:http://utf-8.jp/

コメント

コメントの記入