Hatena::ブログ(Diary)

ockeghem(徳丸浩)の日記 このページをアンテナに追加 RSSフィード

[PR]WAFの導入はHASHコンサルティング
 

2011-12-28

今年読まれた人気記事トップ10+10 今年読まれた人気記事トップ10+10 - ockeghem(徳丸浩)の日記 を含むブックマーク

今年も残りわずかとなり、今年の○○トップ10というタイトルを目にする機会も増えました。徳丸のブログでも、トップ10を発表したいと思います。今年書いたブログ限定ではなく、今年もっとも読まれたブログエントリのトップ10です。

  1. Apache killerは危険〜Apache killerを評価する上での注意〜
  2. PHP5.3.7のcrypt関数のバグはこうして生まれた
  3. もし『よくわかるPHPの教科書』の著者が徳丸浩の『安全なWebアプリケーションの作り方』を読んだら
  4. SQLのエスケープ再考
  5. EZwebの2011年秋冬モデル以降の変更内容とセキュリティ上の注意点
  6. PHP5.3.7のcrypt関数に致命的な脆弱性(Bug #55439)
  7. 都道府県型JPドメインがCookieに及ぼす影響の調査
  8. モテるセキュ女子力を磨くための4つの心得「SQLインジェクションができない女をアピールせよ」等
  9. evernoteのテキストをevernote社の管理者にも見えないように暗号化する
  10. 私はいかにしてソフトバンク端末60機種のJavaScriptを検証したか

ページビュー数では、1.の「Apache killerは危険〜Apache killerを評価する上での注意〜」がぶっちぎりでした。Apache Killerへの関心の高さが伺われますね。

2位の、PHP5.3.7のcrypt関数バグも大きな話題となりました。この問題への態度を観察していると、PHPerよりも他のクラスタの方が騒いでいる印象を受けました。なんというか、「真のPHPerはこの程度の問題では動じないのだ」という無言のメッセージを感じた気がしました。現実問題として、crypt関数あんまり使われてないような気がしますしね。

4位には、「SQLのエスケープ再考」がランクインしています。3年半前のエントリですが、継続して読んでいただいています。私にとっても思い入れの深いエントリです。

8位の「セキュ女子力」も多くの方に読んでいただきました。本格的なコピペを書いたのは実は2回目でして、1回目は「Webアプリ脆弱性オタがふつーのSEの彼女に脆弱性世界を軽く紹介(ry」なんですが、元ネタ増田で出してきたのまで真似して増田で発表したという行きがかり上今まで匿名にしていました。あれから三年も経つのですね。もうばらしてもいいでしょう。

ちなみに、11位〜20位は以下の通りです。

  1. PHPのイタい入門書を読んでAjaxのXSSについて検討した(1)
  2. ソフトバンクのゲートウェイ型SSLの脆弱性を振り返る
  3. 「SQLインジェクション対策」でGoogle検索して上位15記事を検証した
  4. ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)
  5. CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例
  6. SQLの暗黙の型変換はワナがいっぱい
  7. KDDIの新GWで「かんたんログイン」なりすましの危険性あり直ちに対策された
  8. PHP5.4のhtmlspecialcharsに非互換問題
  9. 「安全なWebアプリケーションの作り方」電子書籍版9月28日(水)販売開始します
  10. 僕が「ホワイトリスト」を採用しなかった訳

今年は、なんと言っても「「徳丸本」を完成させることができ、多くの方に読んでいただいたことに尽きると思います。多くの方に支えられて初めてできたことです。本当にありがとうございました。来年もよろしくお願いいたします。

[PR]

安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2011-09-07

PHPのイタい入門書を読んでAjaxXSSについて検討した(3)〜JSON等の想定外読み出しによる攻撃〜 PHPのイタい入門書を読んでAjaxのXSSについて検討した(3)〜JSON等の想定外読み出しによる攻撃〜 - ockeghem(徳丸浩)の日記 を含むブックマーク

 昨日の日記の続きで、Ajaxに固有なセキュリティ問題について検討します。今回はJSON等の想定外読み出しによる攻撃です。これら攻撃手法は本来ブラウザ側で対応すべきもので、やむを得ずWebアプリケーション側で対応する上で、まだ定番となる対策がないように思えます。このため、複数の候補を示することで議論のきっかけにしたいと思います。

 まず、作りながら基礎から学ぶPHPによるWebアプリケーション入門XAMPP/jQuery/HTML5で作るイマドキのWeから、Ajaxを利用したアプリケーションの概念図を引用します(同書P20の図1-23)。

f:id:ockeghem:20110903171555p:image

 前回前々回は、(5)のHTTPレスポンスの前後で、JSON等のデータ作成(エンコード)に起因するevalインジェクションや、(5)のレスポンスを受け取った後のHTMLレンダリングの際のXSSについて説明しました。

 しかし、問題はこれだけではありません。正常系では(5)はXMLHttpRequestオブジェクト(以下XHR)により受け取るレスポンスですが、このレスポンスをXHR以外の方法で呼び出してクロスサイト・スクリプティング(XSS)等を起こす攻撃手法があります。

 以下順に説明します。

JSONの直接ブラウジングによるXSS

 この手法は、サイト利用者(被害者)のブラウザJSONを直接表示させ、JSONに仕込んだJavaScript等を表示・実行させることによるクロスサイト・スクリプティング(XSS)です。

 この攻撃が成立するにはいくつかの条件があります。

  1. ブラウザからの直接リクエストを受け付けること
  2. レスポンスがブラウザによりtext/htmlと解釈されること
  3. レスポンス中にJavaScript等で攻撃文字列を書き込めること

 前回のエントリで私が *勝手に* 手直したrevxmasjson.phpはこれらの性質を満足します。以下に、主要部分を再掲します。

$rows = mysql_query("select * from xmastran where book_id = '$book_id'");
if($rows > 0){
  $row = mysql_fetch_assoc($rows);
  //header("Content-Type: text/javascript; charset=utf-8"); 
  echo json_encode($row);
}else {
  echo "参照不成功:book_id = ".$book_id;
}

 前記(A)と(B)を満たしていることは明らかです。(B)に関しては、header関数でContent-Typeを指定しているのに、残念なことになぜかコメントアウトされています。

 (C)に関しては検討が必要です。json_encode関数デフォルトで、「"」、「\」、「/」をエスケープします。攻撃者はタグの記述に用いる小なり記号や大なり記号は使えますが、閉じタグが使えないので、script要素にJavaScriptを書くことができません。しかし、イベントハンドラJavaScriptを記述することで攻撃可能です。攻撃文字列の例を以下に示します。

<body onload=alert(1)>

 結果は、以下のようにJavaScriptが起動しています。

f:id:ockeghem:20110904202109p:image


対策の考え方

 この問題の対策としては、前述の「攻撃が成立するための3条件」のいずれかを潰すことです。以下のいずれかでよいことになります。

  1. ブラウザからの直接リクエストを受け付けない
  2. レスポンスがブラウザによりtext/htmlと解釈されないようにする
  3. レスポンス中にJavaScript等で攻撃文字列を書き込めないようにする

 これらのうち、脆弱性の根本原因は(B)ですので、B、A、Cの順に説明します。

レスポンスがブラウザによりtext/htmlと解釈されないようにする(B)

 JSONなどのデータ形式のレスポンスによりXSS攻撃ができてしまう根本的な原因は、HTMLとして解釈されることを想定していないコンテンツをブラウザHTMLと誤認してしまうところにあります。従って、HTMLと誤認しないようにするのが対策の本筋です。ということで、まずはContents-Typeを正しく設定することが基本になります。JSONの場合は、RFC4627により、Content-Typeは application/json と定められています。

  • Content-Typeをapplication/jsonに設定する(必須)

 ところがこれだけでは実はダメです。IEが、Content-Typeヘッダを無視してファイルの内容によりコンテンツの形式を決定するという仕様があるためです(参考: 教科書に載らないWebアプリケーションセキュリティ(2):[無視できない]IEのContent-Type無視 (1/2) - @IT)。IE8以降では、以下のレスポンスヘッダにより、「ファイルの内容によりコンテンツの形式を決定する」ことが禁止されますが、IE7以前では効果がありません。

X-Content-Type-Options: nosniff

 従って、Content-Type: application/jsonとX-Content-Type-Options: nosniffの2つのレスポンスヘッダは正しく出力すべきですが、これだけでは、IE7以前の利用者に対応できません。そこで、AまたはCの方法を併用することになります。

ブラウザからの直接リクエストを受け付けない(A)

 Aについては、POSTのパラメータに秘密情報(トークンなど)を渡す*1か、HTTPヘッダに固有の文字列を埋め込んで、サーバー側でチェックするという実装が考えられます。jQueryprototype.jsなど主要なJavaScriptライブラリは、自動的に以下のHTTPリクエストヘッダを付与するので、サーバー側でこれをチェックすることで、XHR以外からの呼び出しを制限できます。

X-Requested-With: XMLHttpRequest

 チェックのためにはサーバー側に以下のようなスクリプトを追加します。呼び出し側(JavaScript)は変更ありません。

if ($_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
  die(json_encode(array('stat' => "参照不成功:不正な呼び出し")));
}

レスポンス中にJavaScript等で攻撃文字列を書き込めないようにする(C)

 Cについては2種類のアプローチが考えられます。

  • JSONのエスケープ時に、「<」や「>」もエスケープ対象とする
  • JSONとして返すデータをあらかじめHTMLエスケープしておく

 前者に関しては、json_encodeのオプションパラメータの指定により実現できます。

  echo json_encode($row, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

 呼び出し例を以下に示します。

<?php
  $a = array('x'=>'<>\'"&');
  echo json_encode($a, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
【実行結果】
{"x":"\u003C\u003E\u0027\u0022\u0026"}

 さらには、「英数字以外すべてエスケープする」、「英数字も含めて全ての文字をエスケープする」という「過剰エスケープ」も候補として考えられます。これらは、後述する「UTF-7を用いた攻撃」にも有効です。

 後者に関しては、第1回で説明したHTMLエスケープのタイミングと関連します。第1回の説明では、データ処理の原則としてJSONでは生データを渡して、HTML表示の直前にエスケープする(テキストとして表示する)のがよいと書きましたが、安全性を重視してJSONのデータ生成時にあらかじめHTMLエスケープしておくという実装も用いられます。

 「安全性を重視して」というのは、JSONの直接表示に備えると言う意味と、JavaScriptによる表示の際の対策漏れという意味があります。


JSONハイジャック

 次にJSONハイジャックという手法について説明します。今回のサンプルは秘密情報を扱わないのですが、仮にJSONを返すサーバーが認証を要求していて、Cookieにより認証を確認した後で結果のJSONを返す仕様だとします。

 この場合に、罠のページからscript要素を用いてJSONにアクセスすることにより、機密情報としてのJSONデータを盗む手法が考案されました。この手法JSONハイジャックといいます。JSONハイジャックに用いる罠サイトは以下のような構成です。

これは罠サイト
<script>
  ここにJSONを読み出す仕掛けを置く
</script>
以下で機密情報のJSONにアクセスする
<script src="http://example.jp/getjson.php"></script>

 この場合、JavaScriptは罠サイトのドメイン上で動作しますが、script要素で読み出したgetjson.phpのリクエストには、example.jpドメインCookieが送信されるので、Cookieのみで認証の管理をしている場合、JSONのデータは正常に読み出せることになります。

 しかし、ここまでなら問題ないはずなのです。JavaScriptとしてみた場合、JSONは単なるオブジェクトのデータにすぎません。それがJavaScript上のソースとしておかれても、罠から読み出す方法がないからです。この様子を以下に模式的に示します。

これは罠サイト
<script>
 ここにJSONを読み出す仕掛けを置く
// script要素で読み出したJSON
[{"name":"Yamada", "mail":"yama@example.jp"}]
</script>

 罠サイトの末尾に、var x = などと書いても、シンタックスエラーになるだけです。

 ところが、このような「ソース上にごろんと置かれたオブジェクト」を読み出す方法が考案され、JSONハイジャックと呼ばれます。JSONハイジャックを実現する手法として、Arrayオブジェクトコンストラクタを再定義する方法やsetterというものを使う方法がありますが、ここでは後者を紹介します。Firefox3.0.x以前で動作します。

<body onload="alert(x)">
罠サイト
<script>
var x = "";
Object.prototype.__defineSetter__("user",
  function(v) {
    x += 'user = ' + v + "\n";
  });
Object.prototype.__defineSetter__("mail",
  function(v) {
    x += 'mail = ' + v + "\n";
  });
// 以下がJSONデータ
[{"user" : "Yamada", "mail":"y@example.jp"},{"user" : "Tanaka", "mail":"t@example.jp"}]
</script>
</body>

 実行結果は以下となります(Firefox3.0.12で検証)。

f:id:ockeghem:20110907090811p:image

 このようなJSONハイジャック攻撃は、現在主要ブラウザの最新バージョンでは動作しないようブラウザ側で対策がとられています。すなわち、ObjectのsetterやArrayのコンストラクタの再定義が禁止されています。しかし、はせがわようすけ氏に教えていただいたところによると、Androidブラウザでは、まだ上記のコードが動きます。手元のXperia arcで確認したところ、確かにJSONハイジャックが動作します。

 Android端末のシェアを考慮すると、まだJSONハイジャックは現実的な脅威として、対策が必要と考えられます。


JSONハイジャックが成立する条件と対策の考え方

 JSONハイジャックが成立する必要条件として以下があります(AND条件)。

 これらから、対策の方針が導かれます。以下のいずれかを実施すればよいことになります。

 script要素からのリクエストにレスポンスを返さない方法の一つとして、POSTメソッドにのみ応答するというものがありますが、私は好みません。GETとPOSTの使い分けはRFC2616で定められており、参照系のリクエストはGETを使うべきとされています。他に手段がないのならともかく、JSONハイジャック対策だけのためにPOSTメソッドにするのはよくないと思います。このため、先に紹介したトークンあるいはリクエストヘッダX-Requested-Withを利用する方法が有効です。

 JSONデータをJavaScriptとして実行できない方法としては、以下の二つの方法が説明されているようです。

  • JSONデータの先頭にwhile(1); などを書いて、script要素から呼ばれた場合無限ループになるようにする
  • JavaScriptとして文法的にエラーにする。例えば、先頭の「[{」を除去しておき、Ajaxで読み出し後にスクリプト側で補う

 これらの方法は現実に使われていて、例えばGmailで返されるJSONデータの先頭にはwhile(1); が入っています。しかし、これらはいかにもバッドノウハウという感じで、個人的に使いたくない気がします。

 また、jQueryJavaScriptライブラリの機能をフルに使おうとすると、これらの方法は使いづらいと考えられます。


その他の攻撃:UTF-7と誤認させる方法

 IE7以前においては、罠ページのscript要素に文字エンコーディングとしてUTF-7を指定することで、機密情報を漏洩させる手法が知られています。詳しくは、はせがわようすけ氏の解説をご覧下さい。

 対策としては、前記と関連して、以下がよいと思います(いずれか、あるいは両方)。

  • X-Requested-Withヘッダのチェック
  • JSONエスケープ時にプラス記号「+」もエスケープする

結局どうすればよいか

 JSONデータを*想定外の*呼び出しによりXSS等の攻撃ができることを示しました。これらは、本来ブラウザ側で対応すべきものをWebアプリケーション側で対応せざるを得ないという意味合いがあるため、対策についても複数の案があります。ここで、これらをまとめます。

直接ブラウズXSSJSONハイジャックUTF-7による攻撃
Content-Type×
Content-Type と X-Content-Type-Options: nosniff×(IE7以前)
X-Requested-Withヘッダのチェック
「<」「>」のエスケープ××
「<」「>」「+」のエスケープ×
HTMLエスケープ××
JavaScriptとして不完全なJSON×
while(1);等×
POSTのみ許容×
攻撃対象ブラウザIEAndroidIE7以前

 現実的な対策案を検討するにあたって、以下のように考えればよいと考えます。

 前述のように、これらは本来ブラウザ側で対応するべきものをWebアプリケーション側で対応するわけですから、アプリケーションとして本来実装すべき内容の延長として無理なく対応できるものがよいと考えます。逆に、HTTPの使い方として無理があるもの(POSTの強制)や、壊れたJavaScript無限ループとするもの(while(1); など)は採用したくないと考えます。

 さらに上表に示す対策のカバー範囲をあわせて考えて、以下で対応することを提案致します。

  • 対策の基本はX-Requested-Withヘッダのチェック
  • Content-Typeは正しく application/json; charset=UTF-8 と指定
  • IEに顔を立てて、X-Content-Type-Options: nosniff を指定(あるいは不要か?)
  • JSON生成ライブラリで設定できる最大限のエスケープ

 PHPによる実装例を示します。

header("Content-Type: application/json; charset=UTF-8");
header("X-Content-Type-Options: nosniff");
if ($_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
  die(json_encode(array('stat' => "参照不成功:不正な呼び出し")));
}
// ...様々な処理
echo json_encode($row, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

 残る課題としては、HTMLエスケープをサーバー側で行うか、クライアント側で行う(エスケープする、あるいはタグ生成しない)かがあります。これについては、どちらかとは決められないような気もしますが、少なくとも、プロジェクト毎にはルールを決めておかないと、過剰エスケープ等バグや、脆弱性の原因になります。

 Ajaxセキュリティについては、これまで攻撃手法の紹介や、それらに対する個別のバッドノウハウ的な対策が紹介されることが多く、体系的・総合的な対策が十分に議論されてこなかったように思います。本エントリが、これら議論のきっかけになれば幸いです。

[PR]

*1URLに秘密情報を渡すことは好ましくありません

Base64Base64 2011/09/27 17:58 base64エンコードされたjson文字列を出力するのは対策としてどうなのでしょうか

ockeghemockeghem 2011/09/29 15:44 過剰エスケープはクライアント側で特別なことをしなくても読めますが、Base64エンコードだと安全だとは思いますが、クライアント側で「余計なこと」をしないといけないですね。あと、デバッグの手間が余計に掛かるのが難点です。

2011-09-06

PHPのイタい入門書を読んでAjaxXSSについて検討した(2)〜evalインジェクション〜 PHPのイタい入門書を読んでAjaxのXSSについて検討した(2)〜evalインジェクション〜 - ockeghem(徳丸浩)の日記 を含むブックマーク

 昨日の日記の続きで、Ajaxに固有なセキュリティ問題について検討します。タイトルはXSSとなっていますが、今回紹介する脆弱性XSSではありません。

 作りながら基礎から学ぶPHPによるWebアプリケーション入門XAMPP/jQuery/HTML5で作るイマドキのWeから、Ajaxを利用したアプリケーションの概念図を引用します(本書P20の図1-23)。

f:id:ockeghem:20110903171555p:image

 Ajaxアプリケーションでは、XMLHttpRequestメソッド等でデータを要求し、サーバーXMLJSON、タブ区切り文字列など適当な形式で返します。ブラウザJavaScriptでは、データ形式デコードして、さまざまな処理の後、HTMLとして表示します。以下に、Ajaxのリクエストがサーバーに届いた後の処理の流れを説明します。

  1. サーバー側でデータを作成、取得
  2. データ伝送用の形式(XMLJSON、タブ区切り文字列等)にエンコード
  3. HTTPレスポンスとして返す(5)
  4. ブラウザ側で、受け取ったデータをデコードして変数に代入する
  5. ブラウザ側処理
  6. HTMLレンダリングして表示

 前回は、FのHTMLレンダリングの際のXSSを扱いました。HTMLエスケープする場所の問題はあるものの、外見上は普通のXSSと変わらないように見えるものでした。

 今回は、BとDにおける、データのエンコードデコードの箇所に関わる問題を説明します。

本書固有の「<i>」で区切る形式

 本書ではJSONの説明も少し出てくるのですが、実際に使われている形式は、HTMLのi要素「<i>」を用いてデータを区切るというユニークな方法です。以下にデータの例を示します。

30<i>山田<i>ABC<i>234-5678<i>Bコース<i>5<i>2010-04-29<i>2010-04-30

 前のエントリで、「out.split("<i>")」というスクリプトが出てきましたが、この形式をデコードする目的だったわけです。

 データをカンマで区切るCSV形式を採用しなかった理由は、データ中にカンマが出てくる場合などがややこしいと著者が判断したからかもしれません。しかし、データを利用者が登録できるという前提では、「<i>」が入力される可能性はあります。この場合、データが1つずれるということが起こり、状況によっては脆弱性になります。本書の場合、その対処はなされていません。

 データを「<i>」で区切る方法は読者も興味がないと思うので、これに関してはこの程度にとどめます。

JSON形式の説明

 JSON(JavaScript Object Notation)は、JavaScriptオブジェクトの表記法をデータ記述言語として用いるものです。前述のように、本書にはJSONが少し説明されています。本書には「5.8 [PHP]配列連想配列およびJSON」という節が用意されているくらいです。しかし、その後JSONは活用されていません。

 とはいうものの、ダウンロードしたサンプルの方には、JSONを活用した例が入っています。Chap7/revxmasjson.htmとChap7/revxmasjson.phpがそれです。これらは、本書から参照されていないようです。ソースの中味を見ると、かなり痛々しい感じで、完成には至っていません。そこで、私が勝手にこのスクリプトを完成させて、JSONセキュリティ問題の題材とします。

まず、元のスクリプトを引用します。「痛々しい」コメントは削除しました。まずはサーバー側のPHPスクリプトです。

$rows = mysql_query("select * from xmastran where book_id = '$book_id'");
if($rows > 0){
  $row = mysql_fetch_assoc($rows);
  //header("Content-Type: text/javascript; charset=utf-8"); 
  echo json_encode($row);
}else {
  echo "参照不成功:book_id = ".$book_id;
}

 このままでも動かなくはないですが、検索がヒットしなかった場合も参照成功となることや、参照失敗時のレスポンスがJSON形式でないことから、少し手を加えました。但し、新たな問題が混入していますが、それは説明のためです。コメントアウトのままのheader関数は本来必要ですが、その理由は次回に説明します。1行目にSQLインジェクション脆弱性が観察されますが、本題とは異なるので気にしないことにします。

$rows = mysql_query("select * from xmastran where book_id = '$book_id'");
if($rows && mysql_num_rows($rows) > 0) {
  $row = mysql_fetch_assoc($rows);
  //header("Content-Type: text/javascript; charset=utf-8");
  $row['stat'] = "参照成功:book_id = ".$book_id;
  echo json_encode($row);
}else {
  echo '{"stat":"参照不成功:book_id = ' . $book_id . '"}';
}

 2行目の参照成功のチェックに、ヒットした行数の確認を加えたことと、参照成功のステータスをJSONとして返すスクリプトを加えています。参照不成功の場合は単純なJSONなので、json_encode関数を使わずに自前でJSONを組み立てていますが、ここに新たな問題が入っています。このスクリプトが返すJSONの例を以下に示します。

{"book_id":"30","name":"\u5c71\u7530","org":"ABC","addr":"\u677f\u6a4b\u533a","tel":"234-5678","mail":"b@example.jp","course":"B\u30b3\u30fc\u30b9","nums":"5","adddate":"2010-04-29","moddate":"2010-04-30","stat":"\u53c2\u7167\u6210\u529f\uff1abook_id = 30"}

 次にクライアントサイドのスクリプトですが、こちらはさらにイタい感じになっていて、大幅に手を加えないと動きません。以下に引用しますが、コメントアウトされたコードを削除しました。

var query = {};
$(function(){ 
  $("#rev").click(function(){   // id=revのイベントハンドラ
    query["book_id"] = $("#book_id").val();  // XHRのクエリストリング
    $.get("revxmasjson.php", query, show);   // XHRの呼び出し
  });
// 中略
});
function show(out){  // $.getのイベントハンドラ
  alert("out="+out);
  var json = eval(out);
  //var json = eval("("+json+")");
  alert(json);
/* 注:以下他のスクリプトから流用した表示機能のコメントアウトが続く */
}

 デバッグ用のalertが残っていたり、eval時にデータを括弧で囲んだりの試行錯誤の後が伺われます。show関数を以下のように手直ししました。

function show(out){
  var res = eval("("+out+")");
  clearui(); // 画面クリア(別の箇所で定義済み)
  var fields = ['name', 'org', 'addr', 'tel', 'mail', 'course', 'nums', 'stat'];
  $.each(fields, function() {
    if (res[this]) {
      $("#" + this).text(res[this]);
    }
  });
}

 2行目で変数outを括弧で囲んだ理由は、JSONオブジェクトを囲むブレースがブロックと解釈されるのを防ぎ、式として解釈させるための定石です*1jQueryの機能を活用すればもっと良い方法がありますが、後述します。

 その後は、JavaScriptオブジェクトHTMLレンダリングするスクリプトですが、たまたまJSONのキー(テーブルxmastranの列名)と、表示先となるtd要素のid属性が同じなので、それを積極的に使っています。

 正常系の処理結果を以下に示します。多少の手直しで、とりあえずはうまく動いています。

f:id:ockeghem:20110904144302p:image


JSONのエスケープとevalインジェクション

 JSONデータを扱う場合は、JSON形式をはみ出してJavaScriptスクリプトが実行される「evalインジェクション」に気をつける必要があります。PHPのjson_encode関数を使ってJSONデータを作成する場合は、json_encode関数が適宜データをエスケープするので問題ありませんし、仮に問題があればjson_encode関数脆弱性です。しかし、上記にjson_encode関数を使わないで手でJSONデータを作成している箇所があるので、ここをチェックしてみましょう。

 問題の箇所は、私がJSON形式を返すよう修正したした以下の行です。

  echo '{"stat":"参照不成功:book_id = ' . $book_id . '"}';

$book_idを埋め込んでいますが、これはユーザの入力値なので、自由に値を入れることができます。ここに、以下の値を入れてみます。

"+alert(1)+"

生成されるJSONは以下となります。

{"stat":"参照不成功:book_id = "+alert(1)+""}

JSON形式の中に、alert関数が注入されました。この式は、JSON形式としては不適合ですが、JavaScritの式としては正常です。そのため、eval関数で評価した結果、以下のようにalertが表示されます。

f:id:ockeghem:20110904151118p:image

 これはバグではあり避けなければなりませんが、この状態では脆弱性とまでは言えません。その理由は、このバグを攻撃に使う経路がないからです。今のままでは、攻撃者が自らテキストボックスに攻撃文字列を入力して「参照」ボタンを押すと、攻撃者のブラウザ上でJavaScriptを実行できますが、それでは攻撃にはなりません。攻撃とするためには、このアプリケーションの利用者(被害者)のブラウザJavaScriptを動かす必要があります(受動的攻撃)。もしも、この画面が、book_idを外部から指定できてボタンを押さなくても予約情報が表示される仕様であれば、外部からの攻撃に利用でき、脆弱性となります。また、このスクリプトの場合は問題ありませんが、攻撃者が登録した情報を利用者(被害者)が閲覧した際にevalインジェクションが起こる場合も脆弱性です。

対策

 このevalインジェクション問題を避けるためには、JSONデータを組み立てる際の適切なエスケープが必要です。さしあたっては、二重引用符「"」を「\"」とし、バックスラッシュ「\」を「\\」とすべきですが、わざわざ手でエスケープするよりも、json_encode関数を利用すべきでしょう。修正例を以下に示します。

  echo json_encode(array('stat' => "参照不成功:book_id = $book_id"));

 また、クライアント側も、eval関数の直接呼び出しを避け、jQueryの機能を活用するとよいでしょう。jQueryJSON形式のデータを受け取ると、内部で文字列からJavaScriptオブジェクトに変換してくれる機能があるので、それを活用します。その方法にはいくつかありますが、例えば、getメソッドをgetJSONメソッドに変更することです。これに伴い、evalの呼び出しも不要なので削除します。

    $.get("revxmasjson.php", query, show);   // XHRの呼び出し
   ↓
    $.getJSON("revxmasjson.php", query, show);   // XHRの呼び出し

function show(out){
  var res = eval("("+out+")");
     ↓
function show(res){
  // evalは不要となる

 このように、evalの明示的な呼び出しを避け、jQueryのgetJSONメソッドを用いると、JSONとしての形式チェックが働くため、evalインジェクションが防止されます。

 このように、JSONエンコードデコードロジックをハードコーディングせず、できるだけPHPjQueryの機能を活用することをお勧めします。上記のどちらか一方でevalインジェクション脆弱性は回避できますが、両方の実施を推奨します。

 しかし、実はそれだけでは別の問題が残ります。これについては、次回に説明します。

まとめ

 Ajax/JSONでは、XMLJSONCSVなどのデータ形式を利用してデータの受け渡しを行いますが、その際に適切なエスケープを怠ると、脆弱性の原因になることを示しました。典型的には、JSONのevalインジェクションの問題があります。

 対策は、エンコードデコードともに適切なライブラリ関数を利用することです。

補足

 私が追記した部分で、以下のコードは少し気持ち悪いなーと思いながら書いていました。

  $("#" + this).text(res[this]);

 textメソッドの呼び出しは常に安全ですが、$("#" + hogehoge)という部分は常に安全というわけではないからです。このあたり、詳しくは、以下のブログエントリをご覧下さい。

 例をあげて説明しましょう。以下はid指定ではなく、s要素の作成となります。つまり、「<s>this is a pen</s>」に相当するオブジェクトができるわけですね(タグの前の#等の文字列は無視されるようです)。

  $("#" + "<s>").text("this is a pen.");

 これだけだと、単にオブジェクトが生成されるだけなので脆弱性にはならないのですが、ばけらさんのエントリにあるように、「onerrorイベント付きのimg要素を作らせるような技」により、スクリプトを実行させられます。実例を以下に示します。

 var id = '<img src="!" onerror="alert(1)">';
 $('#'+id).text(text);

 画像ファイル「!」が存在しない場合、onerrorイベントでalert関数が動きます。

 さて、元のスクリプトは「気持ち悪い」ながらも3行上まで遡れば問題ないことがわかります。もし仮に、もっと広範囲をチェックしなければ正しいことを確認できないならば、このコードは採用しなかったと思います。一般論として、プログラムをチェックする範囲を限定することによりバグを出にくくすることができますが、バグの一種である脆弱性についても同じことが言えます。


[PR]

*1:本書P144にはJSONの動くサンプルが記載されていますが、こちらは配列形式のJSONなので括弧で囲む必要がないのでした

2011-09-05

PHPのイタい入門書を読んでAjaxXSSについて検討した(1) PHPのイタい入門書を読んでAjaxのXSSについて検討した(1) - ockeghem(徳丸浩)の日記 を含むブックマーク

 このエントリでは、あるPHPの入門書を題材として、Ajaxアプリケーション脆弱性について検討します。全3回となる予定です。

このエントリを書いたきっかけ

 twitterからタレコミをちょうだいして、作りながら基礎から学ぶPHPによるWebアプリケーション入門XAMPP/jQuery/HTML5で作るイマドキのWeという本を読みました。所感は以下の通りです。

 今時この水準はないわーと思いました。以前私がレビューした本は、最低でも、XSSSQLインジェクションの二大脆弱性については一応の配慮があり、しかし漏れがあったり、他の脆弱性があったりという箇所を指摘していました。それに対して、本書は、セキュリティへの配慮は、見事なほどにありません。本書の索引には「セキュリティ」という項目はありますが、それはXAMPPセキュリティ設定に関するもので、アプリケーション脆弱性についての説明は一切ありません。

 タレコミ氏からはレビューをお願いされたのですが、あまりに基本的な対策漏れを淡々と指摘しても、読者にとって有益な情報を提供できないと思いました。その一方で、本書ならではの視点が提供できることに気がつきました。それは、本書が基本的に静的HTMLAjaxにより動的コンテンツを提供している点です。

 AjaxアプリケーションXSSは、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」では紹介していません。同書執筆時点で、AjaxXSSに対してどう対策するべきなのか、十分整理できていなかったためです。そこで、議論のきっかけとして、本書のAjaxを用いたアプリケーションXSSと対策案を紹介したいと思います。

 本書のアプリケーションは下図(本書P20の図1-23から引用)に示す構成です。

f:id:ockeghem:20110903171555p:image

 まず、(1)でHTML5JavaScriptによる画面がブラウザに表示されます。画面からは、XMLHttpRequest(jQeuryによる実装)によりPHP記述のAPIを呼び出し(2)、MySQLクエリー(3, 4)の結果を返します(5)。受け取った結果は、DOMを用いて表示されます(jQueryによる実装)。基本的なAjaxアプリケーションの構成を押さえたものであり、意欲的な構成ですね。

 今回は、まず基本的な画面表示のXSSを紹介します。


画面表示箇所(DOM)のXSS対策ができている例

 狭義のXSSは、最後のDOMによる表示箇所のXSSということになりますが、意外なことに「セキュリティを全く意識していない」割にはこの部分のXSSは比較的少ないのです。それは、jQeuryのtextメソッドを用いているために、自動的にエスケープされているからです*1。その例をまず示しましょう。以下は、「Xmasパーティ予約情報参照」という画面の表示処理の部分です(本書P176リスト6-4より引用)。

function show(out){
  var res = out.split("<i>");
  $("#name").text(res[0]);
  $("#org").text(res[1]);
  $("#addr").text(res[2]);
  $("#tel").text(res[3]);
  $("#mail").text(res[4]);
  $("#course").text(res[5]);
  $("#nums").text(res[6]);
  $("#stat").text(res[7]);
}

 このスクリプトの実行例を下図に示します。textメソッドにより、小なり記号「<」などが適切にエスケープされ、XSS脆弱性はありません(下図)。

f:id:ockeghem:20110903173231p:image


画面表示箇所(DOM)のXSS対策ができていない例

 一方、画面表示箇所のXSS対策ができていない場合もあります。それは、table要素の組み立てなど、HTML断片を文字列連結で組み立てている箇所です。例を引用します(P209リスト7-4)。

var head = "<table border='1'><tr>";
head+="<th>ID</th>";
head+="<th>代表者名</th>";
head+="<th>所属組織</th>";
head+="<th>電話番号</th>";
head+="<th>コース</th>";
head+="<th>人数</th>";
head+="<th>登録日時</th>";
head+="<th>更新日時</th>";
$(function(){ 
    $("#rev").click(function(){
        $.get("revallxmas2.php", {}, show);
    });
});
function show(out){
    var body = "";
    var recs = out.split("<r>");
    for(var i = 0; i <recs.length; i++){
        body += "<tr>";
        var flds = recs[i].split("<i>");
        for(var j = 0; j <flds.length; j++){
            if(j!=5){
                body += "<td>" + flds[j] + "</td>";                 // ← ココ
            }else{
                body += "<td align='right'>" + flds[j] + "</td>";   // ← ココ
            }
        }
        body += "</tr>";
    }
    body += "</table>";
    $("#show").html(head+body);
}

 「ココ」とコメントしたところにXSS脆弱性があります。組み立てられたHTML断片は最終的にhtmlメソッドで描画されますが、flds[j]はHTMLエスケープされていないことがXSS脆弱性の原因となっています。この箇所では、「<td>」というタグとflds[j]という生データを文字列連結していますが、この時点でXSSがあることは確実ですね。

 実際にXSSが起こっている画面例を以下に示します。alertの表示の他、打ち消し線の表示で、XSS脆弱性があることがわかります。

f:id:ockeghem:20110903234710p:image

 次にこのスクリプトXSS対策してみましょう。表示の際にHTMLエスケープすればよいわけですが、サーバー(PHP)側でエスケープする方法と、JavaScript側でエスケープする方法があります。ここでは、表示の際にエスケープするという原則に従って、JavaScriptでエスケープすることにします。この問題は最後でもう一度議論します。

 元のスクリプトjQueryの機能を十分使っていないように思われるので、もう少しjQueryを活用する形で書いてみました。jQueryの使い方などでツッコミがあれば歓迎します。

function show(out){
    var table = $("<table border='1'><tr><th>ID</th><th>代表者名</th><th>所属組織</th><th>電話番号</th><th>コース</th><th>人数</th><th>登録日時</th><th>更新日時</th>");
    var recs = out.split("<r>");
    for(var i = 0; i < recs.length; i++) {
        var flds = recs[i].split("<i>");
        var row = $("<tr>");
        for(var j = 0; j < flds.length; j++) {
            if(j != 5) {
                col = $("<td>").text(flds[j]);
            } else {
                col = $("<td align='right'>").text(flds[j]);
            }
            row.append(col);
        }
        table.append(row);
    }
    $("#show").empty().append(table);
}

 ご覧のように、table、行(変数row)、列(変数col)を用意して、appendメソッドで付け足していくという方法をとりました。テキストはtextメソッドで埋め込んでいるので、jQuery側でエスケープしてくれます。

 同じデータで、対策版では以下のように表示されます。小なり記号「<」などがエスケープされている様子がわかります。

f:id:ockeghem:20110903234709p:image


HTMLエスケープをどこで行うか

 通常のWebアプリケーションの場合、HTMLで表示するパラメータのエスケープはサーバー側で行うしかありませんが、Ajaxアプリの場合、以下の選択肢があります。

 原則論から言えば、サーバーから生データを送るのが正しいように思います。Ajaxで受け取ったデータは表示するだけでなく、さまざまなデータ処理に活用する可能性があり、その場合は生データが便利です。また、表示の直前にエスケープするという原則にかなうからです。

 この問題については、id:malaさんの以下のブログエントリも参考になります(Ajaxがテーマではありませんが共通する話題です)。

参考:HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について - 金利0無利息キャッシング ? キャッシングできます - subtech


まとめ

 Ajaxアプリケーションの単純なXSSについて説明しました。Ajaxを活用したアプリケーションであっても、HTMLとして出力するデータのエスケープは必要というあたりまえのことですね。

 次回は、Ajaxとして受け取るデータの問題(XSSではなく、evalインジェクション等)について説明します。


[PR]

*1id:mala さんから指摘をいただき、textメソッドは自動エスケープというより、タグが生成されないと説明した方が良いとのことでした。確かにその通りだと思います。また、以下の説明も同様です。

2011-07-06

私はいかにしてソフトバンク端末60機種のJavaScriptを検証したか 私はいかにしてソフトバンク端末60機種のJavaScriptを検証したか - ockeghem(徳丸浩)の日記 を含むブックマーク

 昨日のno titleで報告したように、昨年5月に、ソフトバンク60機種の検証を行い、JavaScript対応の状況などを調査しました。当時はまだ公式なJavaScript対応機種はない状態でしたが、既にほとんどの端末が *非公式に* JavaScriptに対応していました。

 このエントリでは、検証の様子を報告します。

なぜJavaScript対応状況を調査したか

 403 Forbiddenを公表した前後に、とある方(この方)から、ソフトバンクのケータイでもJavaScriptが動作すると伺いました(参考のやりとり)。XMLHttpRequestも含めてJavaScrptが動くと教えていただいた932SHを私も購入して調べたところ、以下が判明しました。

  • 確かにJavaScriptXMLHttpRequestが動く
  • DNSリバインディング攻撃も成立する
  • なんとHostヘッダが書き換えできる。リリースで推奨した対策が使えない

 困りました。回避策がないので公表ができません。そのため、ツテをたどってソフトバンクモバイルのセキュリティ担当者に連絡をとっていただき、12月初旬に打合せを持ちました。打合せ自体は紳士的かつスムーズに進み、対応の検討をいただくことになりました。帰り際に、当面この件は秘密にするがあまりに対応に時間がかかるようであれば公表するかもしれないと申し上げました。

 その後約半年後に、WASForum2010にてドコモ端末でのDNSリバインディング攻撃について発表することになりましたが、ソフトバンク端末にももっと深刻な問題があることを伏せて、ドコモ端末の問題のみを取り上げるのも心苦しい気がしました。

 そこで、あらためてソフトバンク端末でのDNSリバインディング攻撃の影響について評価し直し、影響が軽微であると評価できれば公表することにしました。

まずはマニュアルの調査

 最近の携帯電話のマニュアルはPDF等で公開されており、ソフトバンク端末のマニュアルも公開されています。そこで、約100種類の端末についてマニュアル*1を調査しました。調査のポイントは以下の通りです。

 その結果、以下のことが分かりました。

  • シャープ製の端末はXMLHttpRequestに対応しているがデフォルトでは無効になっているものが多い
  • シャープ以外の端末はXMLHttpRequestに関する記述がなく、機能自体がないようにも思えるが、ないと断言はできない

 XMLHttpRequestデフォルトで無効になっている端末だけであれば、NTTドコモの端末向けの対策で対処できるので新たな影響はないと考えたのですが、マニュアルの記述だけでは、全ての端末でXMLHttpRequestデフォルト状態で無効になっているかどうかは分かりませんでした。

実機での検証を決意

 マニュアルだけからでは確証が得られなかったので、実機を用いて検証することを決意しました。携帯端末の実機検証用に、全ての携帯端末を保有していて貸し出しをする会社が日本には何社かあります。そのうちの一社に申し込みをして、実機での検証をすることにしました。

 実機検証の方法には何種類かありますが、一番安上がりの方法として、検証センターの会議室を借りて自分で検証する方法を選びました。その場合、端末の台数と、検証の時間で費用が変わります。

 端末の台数については、マニュアルによる調査の感触から、2006年春モデルから2010年春モデルまで、数機種ずつを選択することにして、60台くらいで傾向がつかめると予測しました。また、それ以前の海外製の端末もJavaScriptが動作するものがあると分かっていましたので、検証に加えることにしました。

 検証に要する時間は、1台あたり3分以下で終わらせることとして、60台で3時間という目標にしました。これは、かなり *忙しい* 想定です。そのため、以下に述べるように、時間短縮の工夫をしました。

検証用スクリプトの作成

 短時間で検証が終えられるように、検証用スクリプトを工夫しました。下図に、検証スクリプトの実行例を示します。

f:id:ockeghem:20110706065641p:image

 表示されているように、上から、ダンプファイル名、iframe利用可否、JavaScript利用可否、XMLHttpRequestの利用可否とHostヘッダの書き換えの有無、User-Agentなどが分かるようになっています。

 端末の上側に、インクで821Nと書いているのは、端末をB5サイズのホワイトボード上においてマーカーペンで機種名を書き、全体を写真に撮ることで記録の効率化を図った結果です。

検証用ドメインの取得

 検証を効率化するためには、検証用スクリプトURL入力時間も馬鹿にならないと予想しました。私が既に持っているtokumaru.orgやhash-c.co.jpでは、まどろっこしい気がしました。そのため、検証用にドメインを取得することにしました。

 携帯端末で打ちやすいドメイン名は、テンキーからワンクリックで入力できる文字のみで構成され、かつ文字数の少ないドメインがベストです。具体的には、ドットとA, D, G, J, M, P, T, Wのみで構成されるドメインです。また、同じ文字が連続してもいけません(AAD.JPはダメ)。たまたま.JPドメインはこの条件に当てはまるので、.JPとして取得できるドメインの最小文字数である3文字で、かつ先に述べたアルファベットだけからなるドメインが残っているか調べたところ、幸いまだ数個は残っていました。そのうちの一個を取得しました。これで、ドメインは***.jpと6文字になり、6ストロークのキー入力で検証用スクリプトURLを入力できることになりました。

検証実施

 準備ができたので、2010年5月某日の13時に検証センターに入りました。

 検証センターでは、いきなり60台の端末を貸してくれるわけではなく、四角いトレイに乗った8台程度ずつを預かり、検証が済めばそれを返して次の端末が乗ったトレイを受け取るという仕組みでした。写真を取っておけば良かったのですが、当方も焦っていてそのような余裕はありませんでした。

 検証は全体としては順調に進んだのですが、もっとも手こずったのは携帯電話の操作でした。古いノキア製端末などは、操作方法が独特なものがあり、「これはどうやって使うんだ?」と少しばかり悩みました…が、なんとか分かりました。

 検証結果は、下図の写真のように、紙のシートに記録していきました。ノートPCは一応持って行きました。このノートPCは現地でスクリプトを修正するなどの緊急対応用でしたが、幸いなことに出番はありませんでした。

f:id:ockeghem:20110706065636j:image

 記録用写真の最後のタイムスタンプは15:56。なんとか3時間で検証を終えることができました。

検証結果の例

 下図に検証結果の写真を示します。機種は、先ほどの821Nです。

f:id:ockeghem:20110706065640j:imagef:id:ockeghem:20110706065639j:imagef:id:ockeghem:20110706065638j:imagef:id:ockeghem:20110706065637j:image
JavaScriptの設定画面基本の検証画面ヘッダ改変チェック(HTTP)
Hostヘッダ、Refererが改変できている。
ヘッダ改変(HTTPS)
User-Agent、端末製造番号、
UIDが改変されている

 821Nは、JavaScriptによるヘッダ改変の自由度がもっとも高い機種の1つです。

 ご覧のように、HTTPSではUser-AgentとUIDの改変ができています。User-Agentが変更できるということは、端末製造番号も変更できるということです。

 HTTPSの方が変更できる幅が広いと書くと、「HTTPSは改ざんできないはずなのにどうしてだ?」と疑問に思われる方が出てくると思います。しかし、HTTPSは通信路上での改変を防ぐもので、端末上でのヘッダの変更を妨げるものではありません。さらに、HTTPの場合はゲートウェイでヘッダのチェックや(正しい値への)変更が入っているのに対して、HTTPSの場合はゲートウェイでのチェックや変更ができません。ゲートウェイは端末とWebサーバーの途中にあるので、HTTPSの保護範囲であり、ゲートウェイは素通しになるのです*3

 現在、某銀行において、ソフトバンク端末でオンラインバンキングが使用できなくなっています。アナウンスを読む限り、SSLでケータイIDをチェックしていたようですが、現実には、SSLでケータイIDを認証等に用いることは非常に危険です。ケータイIDを認証に使うこと自体が好ましくないわけですが、仮に使うのであれば、SSLを避けなければなりません。

結果

 検証の結果、820N、821N、830CA、940SCの4機種*4で、デフォルトXMLHttpRequestが使えることが分かりました。これらの端末では、DNSリバインディング攻撃の影響を強く受けるわけですが、幸いこれら機種のシェアは低く、危険性の公開に踏み切った方がマクロで見れば安全性が高まると判断しました。私の発表の後、すぐソフトバンクからもJavaScript停止の案内が出ましたので、情報公開して良かったと思います。

まとめ

 ソフトバンク端末のJavaScript対応状況の一部始終を報告しました。PCと違って、実機を用いないと検証ができないのでやっかいですね。せめて、端末に関する技術情報がもっと開示されるとよいと思います。

 また、ケータイに関する脆弱性を知ってしまうと、取り扱いに窮する場合があります。このあたりの実状は、このエントリの他、高木浩光氏のエントリ高木浩光@自宅の日記 - SoftBankガラケーの致命的な脆弱性がようやく解消も参考になります。

[PR]

*1:容量は1.65Gバイトになります

*2:iframe要素でもDNSリバインディング攻撃は可能ですが、Hostヘッダの書き換えができないのでiframe要素の方はよしとしました。iframeによるDNSリバインディングの実例については、 ケータイtwitter(twtr.jp)においてDNS Rebinding攻撃に対する脆弱性を発見・通報し、即座に修正された - 徳丸浩の日記(2010-02-22)参照。

*3no titleで報告したゲートウェイSSLの場合は別ですが、6月30日に廃止されました

*4:その後831NもデフォルトXMLHttpRequestが使えることが分かりました。831はかんたん携帯なので、完全にノーマークでした。

 
Google