アプリ内XSSの影響と問題点
筆者はSkypeの事例が報道される前にも、いくつかアプリ内XSSを見つけて報告したり、著名なアプリに同様の問題があったことを把握しています。しかし、単にサンドボックス内でJavaScriptコードが実行されるだけだろう、アプリケーションをクラッシュさせたりすることは当然可能だろうが、さほど悪いことはできないだろう、と考えていました。
中には、そのアプリケーションの設定ファイルを読み込むことが可能であり、設定ファイル内にサービスのログインに必要なメールアドレスやパスワードが含まれているケース、あるいはSQLiteデータベースのファイルの中に外部サービスのIDとパスワードが含まれているケースなども見てきました。いずれも影響範囲が大きいものですが、「 そのアプリケーション」の保存しているデータであり、OSの持っているデータは読み取り不可能だろう、それ以外はOS側のサンドボックス機構によって保護されるだろう、と楽観的に考えていました。
しかし、実は深刻な問題だったのです。
脆弱性の影響範囲と修正
自身の経験から、反省を兼ねて学ぶことがあります。バグを見つけたら常に最悪のケースを想定せねばなりません。
最初は「サンドボックス上でJavaScriptが実行されるだけ」という認識を持っていましたが、よくよく調べると、アプリケーションの設定ファイルを盗み出したり、OSのアドレス帳を盗み出したりすることが可能であることがわかりました。そのプラットフォームにおいて、考え得る限りの最悪のコードが実行される可能性を想定しなければならなかったのです。
Sparrowの問題を報告した際、最初はOS X版のみ検証して報告しました。その後、Sparrowの開発者から「iOS版にも同様の問題があるが、サンドボックスによって影響が軽減されるだろう」という回答を受け取りました。多くの開発者は、バグがあった場合にどの程度の危険があるのか、正しく認識することができません。iOS版を検証してみたところ、確かにiOSのアドレス帳を盗み出したり、Sparrowのデータフォルダ経由でSparrowの設定ファイル、アドレス帳、メール本文などを盗み出すことが可能な状態でした。
実際にiOS用のアプリを書いている人であっても、そもそも「アドレス帳のファイルをJavaScriptから直接読む」なんてことを試したことがなく、想定の範囲外であることが想像できます。基本的にはOS側のサンドボックス機構を信頼し、こういった問題について広く認識されてこなかったのではないでしょうか。結果として「何をすると危険なのか、どうすれば安全にできるのか」といった情報は、十分に周知されてきませんでした。多くのエンジニアにとって、
任意のJavaScriptコードが実行されてしまうのはアプリ側の問題
それによってアドレス帳のファイルを盗み出せてしまうのはiOS側の問題
という認識でしょう。
脆弱性への対処姿勢
さて、iOS側の問題であるので対処しなくてよいのか、というと、そういうわけにもいかないでしょう。アプリ側にも問題がある以上は、直さなくてはなりません。ただし影響が大きくなるのはiOS側の問題です。こういった、複合的な要因で影響が大きくなるような問題がある場合「アプリケーション側のバグであるので対処しない。仕様である」といった対応が取られてしまうことが多々あります。
修正されるとしても、いつリリースされるかわからない、すべてのユーザがアップデートするとは限らないため、アプリケーション側での対策が必要となります。先述したように、UIWebViewをそのまま使うとアドレス帳や発着信履歴のファイルを読み取ることが可能になる問題はiOS 6において修正されました。しかし、問題が広く知られてから修正されるまでは、ずいぶんと時間がかかったとも言えます。
HTMLサニタイジング処理
Sparrowの脆弱性を引き起こすきっかけとなったHTMLサニタイジング処理について考えていきましょう。
文字の大きさや色などの指定、画像の貼り付けが可能なリッチテキストとしてHTMLを使いたいが、スクリプトは実行させたくないというケースはちょくちょくあると思います。典型的にはHTMLメールの表示や、フィードリーダ、ある程度自由にタグを使うことができるブログサービス、掲示板などが挙げられます。こういった場合、安全に実装するにはどういうアプローチを取ればよいのでしょうか。
Webアプリケーションの場合は、サーバサイドの各々の言語で、簡単なものであれば正規表現によるフィルタ処理、複雑なルールであればHTMLパーサライブラリを使ってホワイトリスト方式でのサニタイジング処理を行います。
では、ブラウザや埋め込みWebView上でこういった処理を行うにはどうすればよいでしょうか。
アンチパターン
スクリプトの除去を行うにあたって注意したいのは、“ 生きている” DOM(Document Object Model )ノード上で処理してはいけない、ということです。
たとえば、
<img src="dummy" onerror="alert(1)">
といったHTML断片を追加した場合、その時点でdummyというURLへのリクエストが発生し、ファイルが存在しない場合にはalert(1)
が実行されます。
document.createElement("div").innerHTML =
'<img src="dummy" onerror="alert(1)">'
この段階で画像の読み込みが発生し、スクリプトが実行されます。生成されたDOMノードからスクリプトやイベントハンドラを除去する処理を書いても、innerHTMLに文字列を代入した段階でスクリプトが実行されてしまうのです。
HTMLサニタイジング処理を考える
HTMLサニタイジング処理ですが、
ブラウザが用意したメソッドを使う
ブラウザでHTMLをパースしたうえでDOMベースの処理を行う
JavaScriptでHTMLをパースする
といった点を意識することによって、車輪の再発明を避けることができます。
ブラウザが用意したメソッドを使う
ブラウザが用意したメソッドを使う例を取り上げます。
Internet Explorer(以下IE)8以降には、まさにスクリプトを除去した安全なHTMLを作成するためのwindow.toStaticHTMLというメソッドが用意されています。window.toStaticHTMLを使うことで、スクリプトやイベントハンドラが除去されたHTMLを作ることができます 。
IEコンポーネントを使うのであれば、素直にブラウザ側が用意したメソッドを使いましょう。過去には、toStaticHTMLを使っているにもかかわらず、スクリプトの除去が不完全であるという問題がありました。しかし、そういった問題は一時的なものですし、それはあなたのせいではありません。OSのアップデートで修正されます。
ブラウザでHTMLをパースしDOMベースの処理を行う
次に、ブラウザ側の機能を使ってHTMLをパースする方法です。これはcreateHTMLDocumentを使うとよいでしょう。createHTMLDocumentはHTML5仕様で標準化されており、安定して使うことができます。IEでもIE9以降でサポートされています。HTMLパース処理を行い、新たなドキュメントを作ったら、あとはすべてのDOMノードとAttributeを列挙して、許可したタグと属性以外をすべて除去すれば完了です。
JavaScriptでHTMLをパースする
最後にJavaScriptでHTMLをパースする方法です。ブラウザ側での実装に依存せず、JavaScriptのみでHTMLのサニタイジングを行います。このアプローチで実績があるのは、google-cajaライブラリです 。
Pure JavaScriptで書かれたHTMLパーサは、ブラウザの実装に依存せずに動きます。コマンドラインでのテストや、Node.jsなどで再利用することもできるでしょう。
ただし、google-cajaライブラリに含まれるHTMLサニタイザは処理速度に問題があることが知られています。ごく短いHTML断片を処理するうえでは気にならないでしょうが、巨大なHTMLに対して使うのは、あまりお勧めできません。また、ブラウザ側のHTMLパーサとの非互換の問題もあるでしょう。通常は問題にならないでしょうが、文法違反のHTMLの扱いなどで、細かな差異が必ずあるからです。