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

第6回 DOM-based XSS その1

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

DOM-based XSSを防ぐための3つの基本原則

これら,DOM-based XSSを防ぐための基本的な原則をまとめると,以下の3つとなります。

  1. HTMLを組み立てる際には適切なDOM操作関数を選ぶ
  2. リンクを作成するときにはhttp:あるいはhttps:のみとなるようにスキームを限定する
  3. 使用しているJavaScriptライブラリも更新する

これら3つの点について,さらに掘り下げて解説します。

HTMLを組み立てる際には適切なDOM操作関数を選ぶ

DOM-based XSSを発生させやすい最も大きい原因は,JavaScriptによってHTMLを組み立てる際に,innerHTMLやdocument.writeのような「文字列からHTML要素を生成してしまう機能」すなわち先ほど説明したDOM-based XSSにおける「シンク」を利用してしまうことにあります。以下は,先ほど示したコードです。

// bad code
// URL内の # より後ろを表示したい
// http://example.jp/#<img src=1 onerror=alert(1)> のようなURLへ誘導することでXSSが発生する
var div = document.getElementById("info");
div.innerHTML = location.hash.substring(1); 

URL中の#より後ろの文字列(ソース)を,<div>要素のinnerHTML(シンク)に代入していることによって,XSSが発生します。

この場合は,テキストノードとして文字列を表示するようなDOM操作を行うことで,XSSの発生を防ぐことができます。具体的には,

var div = document.getElementById("info");
var text = location.hash.substring(1);
div.appendChild(document.createTextNode(text));

または

var div = document.getElementById("info");
var text = location.hash.substring(1);
div.textContent = text;

のように,innerHTMLではなくtextContentを経由してテキストノードとして取り扱うようにします。

また,テキストノードだけでなく属性値についても,同様に適切なDOM操作を行う必要があります。以下のコードでは,攻撃者が変数textにたとえば「" onmouoser="alert(1)」などを挿入できた場合にはXSSが発生してしまいます。

// bad code
var form = document.getElementById("form1");
var text = "....";   // 変数 text は攻撃者がコントロール可能な文字列
form.innerHTML = '<input type="text" name="key" value="' + text + '">';

この場合の対策も,innerHTMLを使ってHTMLを生成するのではなく,属性値を適切に操作するというものになります。

var form = document.getElementById("form1");
var text = "....";   // 変数textは攻撃者がコントロール可能な文字列
var elm = document.createElement("input");
elm.setAttribute("type", "text");
elm.setAttribute("name", "key");
elm.setAttribute("value", text);  // 属性値を設定する
form.appendChild(elm);

このように,属性値の設定においても,innerHTMLではなく,setAttributeのように対象を限定して適切に操作を行えるAPIを選択することで,DOM-based XSSの発生を抑えることができます。

リンクを作成するときにはhttp:あるいはhttps:のみとなるようにスキームを限定する

テキストノードおよび属性値をDOM操作APIを使って適切に設定していても,URLの生成にjavascript:スキームなどが混入すると,DOM-based XSSが発生することになります。たとえば,以下のようなコードがあったとします。

// bad code
var div = document.getElementById("info");
div.innerHTML = '<a href="' + url + '">' + url + '</a>'; // 変数urlは攻撃者がコントロール可能な文字列

このコードでは,先に説明したように,要素のinnerHTMLにそのまま攻撃者がコントロールできる文字列urlを代入しているため,攻撃者が「" onmouseover="alert(1)」「"><img src=# onerror=alert(1)>」などを挿入した場合にはDOM-based XSSが発生します。そのため,対策としてテキストノードおよび属性値を設定するためにcreateTextNodeやsetAttributeといったAPIを使うと説明しました。実際にそれらを使って書き直したコードが以下のものです。

// bad code
var div = document.getElementById("info");
var elm = document.createElement("a");
// 変数urlは攻撃者がコントロール可能な文字列
elm.setAttribute("href", url);
elm.appendChild(document.createTextNode(url));
div.appendChild(elm);

このコードでは,新たに<a>要素を作り,urlの文字列をhref属性とテキストノードにそれぞれ設定しており,開発者が想定しているとおりのDOM構造を生成しているので,一見するとDOM-based XSSは発生しないように思えます。しかし,攻撃者が変数urlに「javascript:alert(1)」「vbscript:msgbox 1」などを挿入すると,HTMLとしては「<a href="javascript:alert(1)">javascript:alert(1)</a>」などが生成され,ユーザーがこの文字列をクリックした場合には攻撃者の作成したJavaScript(やVBScript)がユーザーのブラウザ上で動作してしまいます。

そのため,URLを表す文字列からリンクを生成する場合には,URLがhttp:あるいはhttps:に限定されるようにスキームを確認する必要があります。

var div; 
var elm;

// 変数urlは攻撃者がコントロール可能な文字列
if (url.match(/^https?:\/\//)) {
    div = document.getElementById("info");
    elm = document.createElement("a");
    elm.setAttribute("href", url);
    elm.appendChild(document.createTextNode(url));
    div.appendChild(elm);
}

このコードでは,変数urlの指している文字列が「http://」あるいは「https://」から始まるかどうかを確認し,そうであった場合にだけ<a>要素を生成し,リンクとして設定しています。これにより,javascriptスキームやvbscriptスキームを利用したDOM-based XSSを防ぐことができます。

この方法では,リンク先のURLとしては「http://」あるいは「https://」から始まる文字列しか使用できません。リンク先のURLとして「/nextpage.html」のような相対URLを許容したいという場合には,連載第5回で説明した方法で与えられたURLをパースして絶対URLに変換し,その絶対URLのプロトコルスキームが「http:」あるいは「https:」かどうかを確認するという方法を採ります。

var div; 
var elm;

// 変数urlは攻撃者がコントロール可能な文字列
var urlObj = createUrl(url);    // URLをパース
if (urlObj.protocol === "http:" || urlObj.protocol === "https:") {
    div = document.getElementById("info");
    elm = document.createElement("a");
    elm.setAttribute("href", urlObj.href);
    elm.appendChild(document.createTextNode(url));
    div.appendChild(elm);
}

ここまで,<a>要素のhref属性にURLを設定する場合を例に,javascript:スキームなどが挿入されてDOM-based XSSが発生する点についての対策を説明しました。しかし,実際のWebアプリケーションでは,<a>要素へのURLの設定だけでなく,locationオブジェクトへの代入やlocation.assignメソッド,location.replaceメソッドの呼び出しなどによって表示しているドキュメントのURLを遷移する場合においても,<a>要素の場合と同様に,javascript:スキームやvbscript:スキームが渡されることを避けなければいけません。

たとえば,以下のようなコードでは,攻撃者がユーザーを「http://example.jp/#javascript:alert(1)」のようなURLへ誘導することによって,DOM-based XSSが発生してしまいます。

// bad code
var nextPage = location.hash.substring(1);   //URL内の#より後ろの部分
location.href = nextPage;

また,locationへの代入などによるページの移動においては,javascript:スキームなどによるXSSだけでなくhttp:やhttps:スキームであっても任意サイトのURLへ移動できると,オープンリダイレクト脆弱性になってしまいます。

そのため,locationへのURLの設定においては,そのURLのオリジンがリダイレクト先として許されるものかどうか――通常は,現在表示しているドキュメントのオリジンであるかどうか――を検査しなくてはいけません。

与えられた文字列をURLとしてパースし,オリジンを取得する方法についても,連載第5回のコードがそのまま利用できます。

var newLocation;
if (location.origin === undefined) {
    location.origin = location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : "");
}

// 変数urlは攻撃者がコントロール可能な文字列
newLocation = createUrl(url);   // URL文字列をパースし,protocolやhost,originなどのプロパティに分解
if (newLocation.origin === location.origin) {
    // 現在表示しているドキュメントと同一オリジンであればページを移動
    location.href = newLocation.href;
}

このコードでは,まずlocationオブジェクトにoriginプロパティを持っていない場合(IEが該当します)には,現在のドキュメントのオリジンをlocation.originプロパティとしてセットします。その後,連載第5回で説明した方法によりURLをパースし,その移動先URLのオリジンが現在のページのオリジンと一致している場合にのみlocation.hrefにその値を設定して,新しいドキュメントへと移動しています。

このようにすることで,ページの移動は現在のドキュメントと同じオリジンに限定され,javascript:スキームなどによるXSSや,任意サイトへのページ遷移によるオープンリダイレクトを防ぐことができます。

リダイレクト先として,現在のドキュメントとは異なるオリジンへの移動を許可する場合には,事前に移動先として許容されるオリジンを固定のリストとして定義しておき,移動先URLのオリジンがそのリスト内に存在するかどうかを確認するという方法を採ります。

var newLocation;
if (location.origin === undefined) {
    location.origin = location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : "");
}
// 移動先として許容されるオリジンのリスト
var allowedOrigins = [ "http://site1.example.jp", "https://site1.example.jp", "http://site2.example.jp", location.origin ];

// 変数urlは攻撃者がコントロール可能な文字列
newLocation = createUrl(url);   // URL文字列をパースし,protocolやhost,originなどのプロパティに分解
if (allowedOrigins.indexOf( newLocation.origin ) !== -1) {
    // リスト内にオリジンが存在するならページを移動
    location.href = newLocation.href;
}

このコードでは,allowedOriginsにリダイレクト先として許可するオリジンとして以下の4つを定義し,リダイレクト先URLのオリジンがこれらに一致する場合のみページを移動しています。

  • http://site1.example.jp
  • https://site1.example.jp
  • http://site2.example.jp
  • 現在のドキュメントのオリジン

使用しているJavaScriptライブラリも更新する

現在,ある程度の規模のWebアプリケーションにおいては,ゼロからすべてのJavaScriptコードを書くということはまれで,たいてい何らかのJavaScriptライブラリ――たとえばjQueryやAngularJSのような――を使っていることが多いと思います。そういったJavaScriptライブラリにおいても,脆弱性が発見,報告されることがあります。過去には,jQuery Mobileに脆弱性があったため,特定のバージョンのjQuery Mobileを使っているサイトすべてにおいてDOM-based XSSが存在するといったこともありました。

DOM-based XSSを防ぐために注意深くコードを書いている人であっても,自身で書いたコードではないために意識から抜け落ち,サービス上で使われているそれらのJavaScriptライブラリの更新をうっかり忘れてしまうことがあるようです。

また,動的にHTMLを生成する,いわゆるWebアプリケーションに属さないような静的なHTMLのみを配信する場合であっても,スマートフォンやPCなどのデバイスごとに最適化された表示を行うためにJavaScriptライブラリを使用している場合もあります。そういった静的なHTMLのみを配信しているWebサーバであっても,使われているJavaScriptライブラリは随時更新する必要があります。

サーバサイドのライブラリやミドルウェアと同様に,こういったJavaScriptライブラリの更新もきちんと行うようにしましょう。

著者プロフィール

はせがわようすけ

株式会社セキュアスカイ・テクノロジー常勤技術顧問。 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/