URLは、Webブラウジングを開始するという意味でも、Webに関連する技術の入り口という意味でも、Webにおける起点そのものです。
攻撃者の視点から見ると、多くの場合、URLは攻撃者自身によってコントロール可能です。その複雑な構造から、プログラム内でURLをデータとして取り扱う際にまちがいが発生しやすく、セキュリティ上の問題点を引き起こしやすい格好のねらい目、攻撃の起点であるとも言えます。
また、Webアプリケーションにおいてセキュリティを確保するためには、特定のリソースがそれぞれのWebアプリケーション自身と同一の保護範囲にあるものなのか、あるいは第三者の用意した可能性があり信用できないものなのかを区別して取り扱う必要があります。この境界条件は、Webアプリケーション自身の動作しているURLを基とした「オリジン」と呼ばれる範囲によって決定されます。
今回は、JavaScriptでのセキュリティを学ぶ第一歩として、URLとオリジンという、Webアプリケーションセキュリティを考えるうえで最も基礎となる技術について説明します。
URLは想像以上に複雑なもの
URLは、Webアプリケーションにおいては「自身の位置を示すためにアドレスバーに表示される」というだけのものではありません。さまざまなリソースへアクセスするための、リソースの位置を示す基本的な情報であり、プログラム上で操作するデータとしても取り扱われます。
JavaScript上でURLをデータとして取り扱う場合、ほとんどの場合にはただの文字列として扱われますが、実際のURLはさまざまな情報を含有するデータの複合体でもあります。
「Webアプリケーションで取り扱うURL」と聞いて多くの人が最初に思い浮かべるのは、http://example.jp/のような特定のWebページを指すようなものでしょう。あるいは、もう少し複雑なhttp://example.jp:8080/foo/bar/image.jpgのようなものかも知れません。
さらに、http://example.jp/というURLのドキュメントを開いている状態では、/foo/bar/image.jpgのような文字列も、相対URLとして有効なものとなります。
プロトコルスキームだけでもさまざまのバリエーションが
上記はいずれもhttpスキームを指すURLですが、Webアプリケーションで取り扱うURLはhttpスキームおよびhttpsスキームに限りません。javascript:スキームやdata:スキームなどもありますし、Internet Explorerではvbscript:スキームも利用可能です。ローカルに保存されているファイルを直接ブラウザで開いた場合には、そのコンテンツのURLはfile:スキームを持つことになります。
さらに、navigator.registerProtocolHandler()を使用すれば、開発者は任意のプロトコルスキームをWebブラウザに登録させ、Webアプリケーションでそのプロトコルスキームを処理できるようにもなります。
プロトコルスキームだけでもこれだけのバリエーションがあり、さらにスキームに応じて以降の各パートの内容も異なるなど、URLは複雑な構造を持ちます。そのために、URLを取り扱う場合には、開発者の意図とは異なる解釈結果となることもあります。また、実際のブラウザの実装においても、仕様とは異なる解釈をされる場合が多くあります。
URLを基準にセキュリティ上の境界条件を定めていると、脆弱性につながる可能性が
URLを基準にセキュリティ上の境界条件を定めたコードを書くと、その構造の複雑さやブラウザごとの挙動差から、脆弱性を作りこむ原因になりがちです。
実際にJavaScript上のコードにおいては、開発者自身が無意識である場合も含め、以下のようにURLを基準としてセキュリティ上の境界条件を定めている場面が多々あります。
オープンリダイレクトが発生しないように、リダイレクト先のURLが現在のURLと同じサイトか調べる
<a>要素のhref属性として設定するリンク先URLがjavascriptスキームやvbscriptスキームでないか調べる
XMLHttpRequestで接続しようとしているリソースが現在のURLと同じサイトか調べる(※ )
こういった場面でURLの取り扱いにまちがいがあると、脆弱性へとつながります。たとえば、以下のようなコードがあったとします。
// bad code
// 与えられたURLが特定のサイトかどうかを判断する
function checkUrl( url ){
var validSites = [ "http://example.jp", "http://example1.jp" ];
var i, site;
for( i = 0; i < validSites.length; i++ ){
site = validSites[ i ];
if( url.substr( 0, site.length ) === site ){
return true;
}
}
return false;
}
このcheckUrlという関数は、コードを書いたプログラマの意図としては「引数urlがhttp://example.jpまたはhttp://example1.jp内のコンテンツを指すものであった場合にはtrueを、そうでない場合にはfalseを返す」というものですが、実際にはurlがhttp://example.jp.attacker.example.com/のようなものであった場合にもtrueが返されます。
そのため、たとえばこの関数を用いてURLのチェックを行い、「 関数の戻り値がtrueのときのみ、location.hrefに代入してリダイレクトする」といった処理の場合にはオープンリダイレクトが発生します。また、「 関数の戻り値がtrueのときのみ、XMLHttpRequestでコンテンツを取得し、innerHTMLへ代入する」という処理を行っている場合には、XSSが発生することになります。
経験を積んだプログラマならこのコードを見た瞬間にまちがいに気づくくらい、わざとらしいコードに見えるかもしれませんが、現実にもこれとよく似たコードは多数存在しています。またこれ以外にも、URLを基準として判断を行う場面において、攻撃者が細工したURLを与えることで意図しない判断結果を引き起こし、セキュリティ上の問題点が発生する場面は非常に多くあります。
具体的な対策方法については、今後の回で解説します。今の段階では、「 URLは考えている以上に複雑であり、それが原因でセキュリティ上の問題が発生することが多い」という点を理解しておいてください。
オリジンを利用してデータを保護する
同一オリジンポリシーとは
Webブラウザ上でセキュリティを確保するためには、特定のリソースが自身のWebアプリケーションと同一の保護範囲にあり信用できるものなのか、あるいは第三者の用意した可能性があり信用できないものなのかを区別して取り扱う必要があります。
たとえば、現在多くのWebアプリケーションでは、動的にHTMLを更新するために、JavaScript上でXMLHttpRequest(XHR)を用いてサーバからデータを取得することが一般的に行われています。その際、もしXHRがリモートのあらゆるサーバから無秩序にデータを取得可能だとすると、機密情報を含むページであっても、攻撃者が用意した罠ページからかんたんに読み込むことができてしまいます。
もちろん、実際のWebブラウザではそのようなことのないよう、データを保護するための境界条件を定めており、リソースが表示されているWebページと同じ生成元の場合にはそのリソースへ自由にアクセスできますが、第三者が生成したリソースに対してのアクセスは制限されています。
より具体的には、リソースの生成元としてURLに含まれるスキーム、ホスト名、ポート番号の組み合わせを「オリジン」と呼び、JavaScriptからはそのJavaScriptが動作しているWebページとオリジンが同一のリソースに対しては制限なしに自由にアクセスできますが、オリジンが異なるリソースに対しては、リソースの提供側が明示的に許可しない限りは内容を読むことができないなどの制限があります。この制限のことを「同一オリジンポリシー(Same-Origin Policy) 」と呼びます。Same-Origin Policyの日本語訳として「同一生成元ポリシー」「 同一源泉ポリシー」といった用語が用いられることもありますが、本連載では「同一オリジンポリシー」という表記で統一します。
そして、オリジンを超えてのリソースへのアクセスなどを「クロスオリジン」と呼びます。
URLに含まれるスキーム、ホスト名、ポート番号の組み合わせをオリジンと定めるルールについての詳細は「RFC 6454 - The Web Origin Concept 」という文書に定められています。ポート番号がURLで明示されていない場合には、プロトコルスキームに応じたデフォルトのポート番号が使用されます。
たとえば、http://example.jp/index.htmlから見ると、http://example.jp/favicon.icoやhttp://example.jp/test.txtは同一オリジンのリソースとなります。一方、https://example.jp/test.txtやhttp://img.example.jp/image.png、http://example.jp:8080/index.htmlなどのリソースはそれぞれスキーム、ホスト名、ポート番号が異なるので、http://example.jp/index.htmlとは異なるオリジンとなります。
http://example.jp/index.html http://example.jp/favicon.ico 同一オリジン
http://example.jp/index.html http://example.jp/test.txt 同一オリジン
http://example.jp/index.html https://example.jp/test.txt スキーム名が異なるので同一オリジンではない
http://example.jp/index.html http://img.example.jp/image.png ホスト名が異なるので同一オリジンではない
http://example.jp/index.html http://example.jp:8080/index.html ポート番号が異なるので同一オリジンではない
複数のリソースが同一のオリジンかどうかを比較するために、リソースのURLからオリジンを決定し、正規化して一意に表現するための方法も、RFC6454にて定められています。以下に、リソースのURLと、そのオリジンの対応の例を示します。
URL オリジン
http://example.jp/index.html http://example.jp
http://example.jp/favicon.ico http://example.jp
http://example.jp:8080/file.txt http://example.jp:8080
https://example.jp/form.html https://example.jp
dataスキームの場合、RFC6454では「それぞれのリソースごとにユニークなオリジンを持つ」と記載されていますが、実際のWebブラウザ上では「dataスキームを含む元ページと同一オリジン」という扱いになることがほとんどです。たとえば、ほとんどのブラウザでは、http://example.jp/index.html上のページに以下のような記述があった場合、ここで実行されるJavaScriptは、HTMLと同一のオリジンとなるため、http://example.jpがalertの結果として表示されます。
<script src="data:text/javascript,alert(location.origin)"></script>
また、fileスキームの場合については、RFC6454にて「実装依存」と書かれているとおり、実際のブラウザでもまさにさまざまな動きとなっています。
一般的なWebアプリケーションであれば、fileスキームを取り扱うことは極めて稀なので、fileスキームが問題になることはあまりないかと思います。しかし、スマートフォン向けにWebViewと呼ばれるブラウザ相当の機能を利用したアプリを開発する場合には、過去何度もfileスキーム経由でローカルに保存されているデータを盗み見られる可能性のある脆弱性が発見されているため、注意が必要です。スマートフォン向けのアプリケーションに特化した話については本連載の範囲を超えますので割愛しますが、くわしく知りたい方は一般社団法人日本スマートフォンセキュリティ協会から発行されている「Androidアプリのセキュア設計・セキュアコーディングガイド 」などを参考にするといいでしょう。
オリジンを境界条件として動作が制限されるもの
実際のWebブラウザ上の機構について、同一オリジンポリシーによってオリジンを境界条件として動作が制限されるものとしては、たとえば以下のようなものがあります。
XMLHttpRequest
XMLHttpRequest(XHR)によってサーバ上のリソースを取得する場合、そのリソースのオリジンが現在JavaScriptが動作しているWebページと同じオリジンの場合には、レスポンスの内容にJavaScriptからresponseTextなどのプロパティを通じて自由にアクセスできます。オリジンが異なる場合には、取得しようとしているリソースの提供側が明示的に許可している場合にのみ、レスポンスの内容をJavaScriptから読み取ることができます。
Canvas
HTML5の<canvas>要素には、drawImageメソッドなどによって、クロスオリジンな画像であっても自由に描画することができます。一方、Canvasに表示された画像のオリジンがそのWebページのオリジンと一致している場合にはgetImageDataメソッドなどを通じてJavaScriptから表示されている画像の内容を取得できますが、オリジンが異なる場合には画像の提供側が明示的に許可している場合にしかJavaScriptからはアクセスできません。
Web Storage
HTML5で導入されたブラウザ上にデータを保存する機構であるWeb Storageでは、データはオリジン単位で保護されます。Web Storage上のデータには同じオリジンからは自由に読み書きができますが、オリジンを超えて読み書きすることはできません。
このように、「 同一オリジンポリシー」によって具体的にどのような機能が制限されるかはそれぞれの機構ごとに異なっており、「 同一オリジンポリシー」という言葉はその制限の総称でしかありません。
オリジン以外の条件で境界条件を定める機構も
一方で、ブラウザ上のセキュリティに関わるすべての機能がオリジンを基準として境界を定めているわけではありません。オリジン以外を基準にしている機構としては以下のようなものがあります。
HTTP認証
基本認証に代表されるHTTP認証では、URL中のパス名を基準に認証情報の送信有無が判断されるなど、オリジンではない要素によってリソース保護の境界が定められています。
Cookie
Cookieは、発行する際にはpathやdomainといった指定が可能であり、またデフォルトではhttpとhttpsで共有されるなど、オリジンではない境界条件に基づいて動作しています。
このように、比較的最近ブラウザに実装された現代的な機能ではデータの保護のためにオリジンを境界条件としているのに対し、オリジンという概念がきちんと整理されるより古くからWebブラウザに備わっていた機構では必ずしもオリジンを境界とはしているとは限りません。ところが、これらのオリジン以外で境界が定まっているさまざまな制約についても、文書によっては歴史的経緯などから「同一オリジンポリシー」と呼ばれることもあるため注意が必要です。
これからあなたが作成するアプリケーションにおいてセキュリティ上の境界条件を設ける場合には、「 ホスト名で判断」「 ホスト内の特定のパスで分離」といった方法ではなく、「 オリジン単位で境界条件を設ける」ほうが好ましいと言えます。仮にオリジン以外の条件でセキュリティ上の境界条件を設けた場合には、ブラウザ内の各機構の同一オリジンポリシーと差が出てくるために、さまざまな面で混乱やまちがいが発生しやすくなるでしょう。