Web 2.0的アプリのセキュリティ:再考「機密情報にJSONPでアクセスするな」

SEの進地です。

2007年1月に投稿した「Web 2.0的アプリのセキュリティ:機密情報にJSONPでアクセスするな」は多くの方にお読みいただきました。誤りも指摘され、元エントリーに改修を加えましたが、かなり読みづらい状態になってしまっています。また、JSON、JSONPのセキュリティに関する新たな話題もSea Surfers MLで議論されているのを読み、自分自身の認識や理解も変化しているので、このエントリーでもう一度JSON、JSONP(+JavaScript)に機密情報を含めることの是非と方策を整理、検討したいと思います。

○JSON、JSONP、JavaScriptによるデータ提供時にセキュリティ対策上留意すべき特徴

JSON、JSONP、JavaScriptによるデータ提供時に留意すべき特徴としてあるのが、「クロスドメインアクセス可能」というものです。JSONPだけでなく、JSON、JavaScriptもクロスドメインアクセス可能であることに注意してください。JavaScript、JSON共に単に<script>のsrc属性に外部ドメインのリソースを指定することでクロスドメインアクセス可能です。他方、データ提供時のフォーマットとしてXMLが使われた場合、<script>のsrc属性では読み込めず、XHR(XMLHttpRequest)で読み込む場合もXHRの「クロスドメインの制約」の為にクロスドメインアクセスはできません。

JSON、JSONP、JavaScriptはそのままでは「クロスドメインアクセス可能」、XMLは「クロスドメインアクセス不可能」ということを頭に入れておいてください。

注意
当然ですが、機密情報を含んだXMLがそのままWebサーバ上で公開されていればそのXMLに直接アクセスすることで機密情報を読み取ることができます。あくまでデータの提供フォーマットとしてXMLが採用された場合に「クロスドメインアクセス不可能」ということであって、データ(JSON、XMLなど提供フォーマット問わず)そのものへのアクセスの可否とは別の議論であることに注意してください。


○クロスドメインアクセスと機密情報漏えい(攻撃が成り立つ前提条件)

クロスドメインアクセスが可能なリソースに機密情報が含まれると、その情報はたとえ認証済みユーザにのみ提供されるような情報であっても漏えいの危険にさらされます。例えば、次の攻撃シナリオを考えてみます。

  1. サイトAにログイン済みの被害者が攻撃者の用意した悪意あるスクリプト(スクリプトBとする)が埋め込まれたサイトBのWebページにアクセスする。
  2. スクリプトBは機密情報を保持したクロスドメインアクセス可能なリソースAにアクセスして、これを取得する。1で被害者はサイトAにログイン済みのため、リソースAには問題なくアクセスできる。
  3. スクリプトBはリソースAから取得した機密情報を攻撃者に(XHR等を使って)送る。

この攻撃のポイントは、2でリソースAがクロスドメインアクセス可能であるところです。リソースAがクロスドメインアクセス不可能であれば、この攻撃は成り立ちません。

クロスドメインアクセス可能なリソースに機密情報が含まれること。これが攻撃の前提条件になります。
従って、リソースがJSON、JSONP、JavaScriptである場合、そのままではこれらはクロスドメインアクセス可能ですから、上記シナリオによる攻撃の前提条件を満たすということになります。他方、データ提供の形式がXMLであればクロスドメインアクセス不可能ですから、攻撃の前提条件を満たさないということになります。


○クロスドメインアクセス対策例

JSON、JSONP、JavaScriptで機密情報を配信する場合、クロスドメインアクセスの対策(=クロスドメインアクセスを不可能にする対策)が必要になります。

以下にSea Surfers MLで議論されていた方法を紹介します。

方法1.JavaScriptとして不完全な状態で返す方法

JavaScriptとして不完全な状態、つまり何らかの構文エラーを含ませた状態でJSON、JSONP、JavaScriptを返す方法です。

例えば、サーバ側は

key1 : value1, key2 : value2 }

というような形で返し、クライアント側はこれをXHRで取って先頭に{を補ってあげてevalします。
攻撃者のスクリプトがこのJSONにアクセスしようとしても構文エラーで利用できませんし、XHRでまず取得してからというように動けば、XHRの「クロスドメインの制約」に引っかかります。

方法2.while(1)、およびコメントアウト法

サーバ側がデータの先頭にwhile(1) {}をつける、または全体をコメントアウトした状態で返し、クライアント側でこれを取り除いてevalする方法です。方法1と考え方としてはほとんど変わりません。while(1)の方法はGMailでも使われています。

方法3.xhr.setRequestHeader( "X-From-XHR", true );

リソースへのアクセスにXHRを利用し、本来のページからの正しい利用と罠ページからの利用に区別をつけるために、XHRのsetRequestHeaderを利用してリクエストヘッダに拡張ヘッダを追加する方法です。

xhr.open( ... ); xhr.setResponseHeader( "X-From-XHR", true ); xhr.send(null);

サーバ側は拡張リクエストヘッダ(この場合はX-From-XHR)がtrue指定されているかどうか確認し、指定されている時だけリソースを返すようにします。クライアント側はXHRで取得したリソースをevalして使います。


方法1~3に共通しているのは、XHR経由でしかリソースを利用できないようにし、これによってクロスドメインアクセスの対策を行うという考え方です。


○まとめ

データの提供フォーマットとしてJSON、JSONP、JavaScriptを採用し、かつこれらが機密情報を含む場合にそのままでは「クロスドメインアクセス可能」であるため、これらがたとえ認証済みユーザにのみ提供されるように作られていても漏えいの危険にさらされることを解説しました。また、JSON、JSONP、JavaScriptをデータの提供フォーマットとして採用した際にそのクロスドメインアクセスの対応方法を紹介しました。

結論としては、「クロスドメインアクセスの対策を施していない状態で機密情報を含むJSON、JSONP、JavaScriptでのデータ提供は行ってはいけない」ということになります。
XML+XHR利用であればクロスドメインアクセスの対策は既に行われていますが、クライアント側での解析が面倒です。逆にJSON、JSONP、JavaScript利用であればクライアント側での解析は楽ですが、XHR利用を強制するために多少リソースへの操作が必要になります。どちらを取るかは手間を考慮したトレードオフになると思いますが、実装ミスのリスクも考慮して、僕個人としては機密情報を含む場合はデータ形式はXMLにするのが良いのではないかと考えています。