「安全なWebアプリケーションの作り方」電子書籍版9月28日(水)販売開始します

このエントリでは「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」(いわゆる徳丸本、#wasbook)の電子書籍版が9月28日(水)に販売開始されるというお知らせをします。

大変要望の多かった徳丸本の電子書籍版ですが、タイトルのように、9月28日(水)の午前中に販売開始されることになりました。以下に諸データを記します。

販売サイト ブックパブ(http://bookpub.jp/
販売開始 9/28(水)午前
定価 2,800円
キャンペーン価格 1,800円
キャンペーン期間 9/28(水)〜10/17(月)の20日

いくつか特記すべき事項があります。

まず、電子書籍の形式ですが、*DRMフリーのPDF* です。従来版元のソフトバンククリエイティブでは、iOSAndroidのアプリという形式で電子書籍を販売したようですが、この徳丸本の電子版から、DRMフリーのPDF形式による電子書籍の販売を開始するそうです。(追記:徳丸本はDRMフリーですが、今後のソフトバンククリエイティブ電子書籍が全てDRMフリーになると決まった訳ではありませんので念のため補足します)

PDFの制約などは以下の通りです。

  • PDF データは、「印刷不可」「本文データのクリップボードへのコピーは可」として作成されています
  • PDF データは、1 人につき1 商品、ご購入ください
  • ページの末尾に購入者本人の名前とメールアドレスが記載されます
  • VMwareFiddlerは添付されないので、オリジナルのサイトからダウンロードしてください
  • 実習用仮想マシンは版元のサイトからダウンロードになります

1人1商品ですので、お一人の商品購入者が、ご自身の複数の端末(PCとiPad等)にコピーすることは可です(版元に確認済み)。逆に、部署で共用するような場合は、紙の本でないと具合が悪いでしょうね。

価格については、紙の本より安めの設定となっておりますが、販売記念キャンペーンとして、発売日から20日間1,800円で販売されます。この機会にぜひ電子版の購入を検討ください。

さて、徳丸本は多くの読者の皆様から高い評価を頂戴しており大変有り難いことですが、欠点の筆頭として書籍の重量(約1kg)を指摘される方が多く、電子化を望む声を多数頂いておりました。これに対して、編集部の方でもかなり早くから電子化の検討は進めていましたが、電子書籍のプラットフォーム(iOSか、PCか等)の検討に時間を要していたようです。
ここに来て、DRMフリーのPDFという形式が採用されたことは、もちろんオライリー社の影響はあると思いますが、なにより、読者の皆様の書籍に対するご支持と、電子化への強い要望をtwitter等目に見える形で示していただいたからではないかと推測しております。本当にありがとうございました。著者の私から見ても、現時点のベストの形で提供できたのではないかと考えています。

私自身とレビュアーの方々は既に先行して電子版を試していますが、中々快適です。普段家内が使っている初代iPadにも入れてみましたが、とても読みやすいです。

ということで、徳丸本の電子書籍版販売開始についてご案内させていただきました。引き続きご愛顧いただければ幸いです。
ありがとうございました。

[PR]

PHPのイタい入門書を読んでAjaxのXSSについて検討した(3)〜JSON等の想定外読み出しによる攻撃〜

 昨日の日記の続きで、Ajaxに固有なセキュリティ問題について検討します。今回はJSON等の想定外読み出しによる攻撃です。これら攻撃手法は本来ブラウザ側で対応すべきもので、やむを得ずWebアプリケーション側で対応する上で、まだ定番となる対策がないように思えます。このため、複数の候補を示することで議論のきっかけにしたいと思います。
 まず、作りながら基礎から学ぶPHPによるWebアプリケーション入門XAMPP/jQuery/HTML5で作るイマドキのWeから、Ajaxを利用したアプリケーションの概念図を引用します(同書P20の図1-23)。

 前回前々回は、(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が起動しています。

対策の考え方

 この問題の対策としては、前述の「攻撃が成立するための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ヘッダを無視してファイルの内容によりコンテンツの形式を決定するという仕様があるためです(参考:[無視できない]IEのContent-Type無視 (1/2):教科書に載らないWebアプリケーションセキュリティ(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で検証)。

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

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

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

  • script要素からJSONデータを読み出せること
  • JSONデータがJavaScriptとして実行した場合にエラーにならないこと。{...} というオブジェクト形式の場合はエラーになる(オブジェクトではなくブロックと解釈される)ので不可。[...]という配列形式はOK
  • 被害者が、Firefox3.0.xやAndroid端末など、JSONハイジャック可能なブラウザを利用していること

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

  • script要素からのリクエストにはレスポンスを返さないようにする
  • JSONデータを、JavaScriptとして実行できない形にする
  • JSONハイジャック可能なブラウザを使わない(利用者側でとれる対策)

 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アプリケーション側で対応せざるを得ないという意味合いがあるため、対策についても複数の案があります。ここで、これらをまとめます。

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

 現実的な対策案を検討するにあたって、以下のように考えればよいと考えます。
 前述のように、これらは本来ブラウザ側で対応するべきものを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]

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

PHPのイタい入門書を読んでAjaxのXSSについて検討した(2)〜evalインジェクション〜

 昨日の日記の続きで、Ajaxに固有なセキュリティ問題について検討します。タイトルはXSSとなっていますが、今回紹介する脆弱性XSSではありません。
 作りながら基礎から学ぶPHPによるWebアプリケーション入門XAMPP/jQuery/HTML5で作るイマドキのWeから、Ajaxを利用したアプリケーションの概念図を引用します(本書P20の図1-23)。

 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属性が同じなので、それを積極的に使っています。
 正常系の処理結果を以下に示します。多少の手直しで、とりあえずはうまく動いています。

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

 JSONデータを扱う場合は、JSON形式をはみ出してJavaScriptスクリプトが実行される「evalインジェクション」に気をつける必要があります。PHPjson_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が表示されます。

 これはバグではあり避けなければなりませんが、この状態では脆弱性とまでは言えません。その理由は、このバグを攻撃に使う経路がないからです。今のままでは、攻撃者が自らテキストボックスに攻撃文字列を入力して「参照」ボタンを押すと、攻撃者のブラウザ上で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なので括弧で囲む必要がないのでした

PHPのイタい入門書を読んでAjaxのXSSについて検討した(1)

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

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

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

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

 タレコミ氏からはレビューをお願いされたのですが、あまりに基本的な対策漏れを淡々と指摘しても、読者にとって有益な情報を提供できないと思いました。その一方で、本書ならではの視点が提供できることに気がつきました。それは、本書が基本的に静的HTMLとAjaxにより動的コンテンツを提供している点です。
 AjaxアプリケーションのXSSは、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」では紹介していません。同書執筆時点で、AjaxXSSに対してどう対策するべきなのか、十分整理できていなかったためです。そこで、議論のきっかけとして、本書のAjaxを用いたアプリケーションのXSSと対策案を紹介したいと思います。
 本書のアプリケーションは下図(本書P20の図1-23から引用)に示す構成です。

 まず、(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脆弱性はありません(下図)。

画面表示箇所(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脆弱性があることがわかります。

 次にこのスクリプト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側でエスケープしてくれます。
 同じデータで、対策版では以下のように表示されます。小なり記号「<」などがエスケープされている様子がわかります。

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

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

 原則論から言えば、サーバーから生データを送るのが正しいように思います。Ajaxで受け取ったデータは表示するだけでなく、さまざまなデータ処理に活用する可能性があり、その場合は生データが便利です。また、表示の直前にエスケープするという原則にかなうからです。
 この問題については、id:malaさんの以下のブログエントリも参考になります(Ajaxがテーマではありませんが共通する話題です)。

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

まとめ

 Ajaxアプリケーションの単純なXSSについて説明しました。Ajaxを活用したアプリケーションであっても、HTMLとして出力するデータのエスケープは必要というあたりまえのことですね。
 次回は、Ajaxとして受け取るデータの問題(XSSではなく、evalインジェクション等)について説明します。


[PR]

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

徳丸浩関係の日記ページビューランキングTOP10(2011年8月)

このサイト、旧徳丸浩の日記新徳丸浩の日記のトータルで2011年8月のページビュートップ10を集計してみました。ブログが分かれているので集計めんどくさい:(

  1. Apache killerは危険~Apache killerを評価する上での注意~ | 徳丸浩の日記
  2. PHP5.3.7のcrypt関数のバグはこうして生まれた | 徳丸浩の日記
  3. PHP5.3.7のcrypt関数に致命的な脆弱性(Bug #55439) | 徳丸浩の日記
  4. もし『よくわかるPHPの教科書』の著者が徳丸浩の『安全なWebアプリケーションの作り方』を読んだら - ockeghem(徳丸浩)の日記
  5. EZwebの2011年秋冬モデル以降の変更内容とセキュリティ上の注意点 - ockeghem(徳丸浩)の日記
  6. そろそろSQLエスケープに関して一言いっとくか~SQLのエスケープ再考~ | 徳丸浩の日記
  7. phpMyAdminにおける任意スクリプト実行可能な脆弱性の検証 - ockeghem(徳丸浩)の日記
  8. PDOにおける一応の安全宣言と残る問題点 | 徳丸浩の日記
  9. 最近発覚したパスワードに関する重大な脆弱性4選 - ockeghem(徳丸浩)の日記
  10. PHPカンファレンス2011にて講演します - ockeghem(徳丸浩)の日記

このランキングはGoogle Analyticsの「タイトル別のコンテンツ」をもとに作成…したのだけど、新徳丸浩の日記はまだAnalyticsのタグを埋め込んだばかりなので、こちらはbloggerの統計情報を元にしています。このため若干の誤差がある可能性があります。
6位に「SQLエスケープ再考 - そろそろSQLエスケープに関して一言いっとくか」は3年前のコンテンツですが、いまだにランクインしているのが、個人的には感慨深いですね。当時は独立したてで暇だったので、色々調査して思うところをブログに書いていたことを思い出します。このエントリの内容が高木浩光氏に見いだされて、WASF2008での講演となり、後にIPA非常勤職員として「安全なSQLの呼び出し方」の執筆参画するに至るという、個人的には思い出深いエントリです。

最近発覚したパスワードに関する重大な脆弱性4選

 最近、パスワードにまつわる重大な脆弱性を見かけることが多いように思いますので、その中から4つを選んで紹介します。既に私のブログで紹介したものや、少し古い問題も含まれます。

PHP5.3.7のcrypt関数がハッシュ値を返さない脆弱性

 crypt関数は、様々なハッシュアルゴリズムによるソルト化ハッシュを返す関数ですが、PHP5.3.7(2011年8月18日リリース)において、crypt関数にMD5を指定した場合、ハッシュ値を返さない(ソルトは返す)バグがありました。私のブログエントリにて説明したように、最悪ケースでは任意のパスワードで認証できてしまう状況があり得ました。

 このバグが混入した経緯については、私のブログエントリPHP5.3.7のcrypt関数のバグはこうして生まれた | 徳丸浩の日記を参照ください。
 8月23日にリリースされたPHP5.3.8にてこの問題は修正されました。

Dropboxに、不正なパスワードでも認証を許す問題

 今年の6月20日Dropboxにバグが混入して、正しいパスワードでなくてもログインできる状態が約4時間続きました。

 Dropboxのような著名で人気のあるクラウドサービスにて、このように重大な問題が発生したことは、クラウド利用のあり方に対する警鐘となりました。

RT-N56U における管理パスワード漏えいの脆弱性

 RT-N56Uは、ASUS製の無線LANルータです。RT-N56U(ファームウェアバージョン:1.0.1.4)では、設定画面にて管理パスワードを参照できますが、そのページを見るのにパスワードが必要ないという問題があります。

 参照:JVNVU#200814: RT-N56U における管理パスワード漏えいの脆弱性

 JVNの説明では、「インターネット経由からの攻撃が可能」とありますが、同じページには「なお、初期設定でこの機器に接続できるのは LAN 内のユーザのみです。」とあるので、設定を変更しない限り、インターネットからこのルータに直接接続することはできないはずです。
 しかし、受動的攻撃によって攻撃が可能です。その代表的な手法として、DNSバインディング攻撃があります。同手法の詳細は、私のブログエントリを参照ください。

 一般的にはDNSバインディング攻撃に対する有効な対策は、パスワードを適切に設定することですが、今回はそのパスワードを閲覧できるので、以下のような攻撃が可能です。例えば、全てのポートに対してインターネットからのアクセスを許す(穴を開ける)ような変更が可能になると考えられます。

  1. 設定画面からパスワードを参照する
  2. 前項で得たパスワードを用いて、管理画面にログインして、設定を変更する

 対策は、ファームウェアバージョンアップです。ベンダーサイトからファームウェアバージョン:1.0.1.4o)をインストールしてください。

OS X Lionで、LDAP認証にした際にパスワードをチェックしない問題

 yebo blogによると、Mac OS X LionにてLDAP認証を用いている場合、不正なパスワードで認証できるようです。但し、Apple社の正式なコメントはまだないようで、出典をたどると、サポートページでのやりとりに行き当たります。ここのスレッドにて、「問題をApple社に送信した」とあるので、yebo blogの「Appleは既に問題を認識しており」はその通りですが、どのような対応となるかは不透明です。
 当面の回避策としては、yebo blogにあるように、LDAP認証を無効にするしかないでしょう。

[PR]
 YAPC::ASIAのトークに応募して採択されました。10/14(金)東工大にて、「Webアプリでパスワード保護はどこまでやればいいか」というテーマでしゃべります。主に、パスワードのハッシュやソルトやストレッチングの話で、たまたまcrypt関数のカバーする領域ですが、YAPCというPerlの祭典でPHPの重大なバグの話をするかどうかは決めていません。

もし『よくわかるPHPの教科書』の著者が徳丸浩の『安全なWebアプリケーションの作り方』を読んだら

 たにぐちまことさんの書かれた『よくわかるPHPの教科書(以下、「よくわかる」)』を購入してパラパラと見ていたら、セキュリティ上の問題がかなりあることに気がつきました。そこで、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方(以下、徳丸本)」の章・節毎に照らし合わせて、「よくわかる」の脆弱性について報告します。主に、徳丸本の4章と5章を参照します。

4.2 入力処理とセキュリティ

 「よくわかる」のサンプルや解説では、入力値検証はほとんどしていません。しかし、入力値検証をしていないからといって即脆弱かというとそうではありません。徳丸本でも強調しているように、入力値検証はアプリケーション要件(仕様)に沿っていることを確認するもので、セキュリティ対策が目的ではないからです。
 「よくわかる」の中で、私が見た範囲で唯一の入力値検証は、郵便番号のチェックをするものです。以下に引用します(「よくわかる」P85)。

if (preg_match("/^\d{3}\-\d{4}$/", $zip)) {

 全体一致マッチの方法として「^」と「$」で挟むという方法をとっていますが、徳丸本P81で説明しているように、$はデータの終わりではなく、行末という意味なので、行末の\nがあってもマッチします。具体例を以下に示します。

var_dump(preg_match("/^\d{3}\-\d{4}$/", "111-1111\n"));
【実行結果】
int(1)

 このため、引用部分は以下のように書いた方がベターでしょう。

if (preg_match("/\\A\d{3}\-\d{4}\\z/", $zip)) {

 詳しくは徳丸本を参照ください。
 また、入力値検証ではありませんが、正規表現の使い方がおかしな箇所を見つけました。以下は「ひとこと掲示板」の投稿からURLを抜き出して、a要素のリンクを生成する関数です(P269)。

// 本文内のURLにリンクを設定します
function makeLink($value) {
    return mb_ereg_replace("(https?)(://[[:alnum:]\+\$\;\?\.%,!#~*/:@&=_-]+)", '<a href="\1\2">\1\2</a>' , $value);
}

 唐突にPOSIX文字クラス[:alnum:]が出てきますし、「よくわかる」の正規表現はpreg_matchで説明しているのに、この箇所はmb_eregを使っています。本書は文字エンコーディングとしてUTF-8を採用しているので、preg系の関数を使えないわけではなく、mb_eregが突然出てくる理由がわかりません。過去の資産からの使い回しでしょうか。「よくわかる」の本文(P270)には以下のように指摘されています。

正規表現の内容は理解するのはかなり難しいと思いますが、Googleで「正規表現 URL」などと検索をしたり「PHP URL リンク」などと検索すれば、サンプルプログラムを見つけることができるので、参考にしてもよいでしょう。

 現実問題として、多くのプログラマがこうやって、コピペでプログラムを作っている現実があるにしても、もう少し注意点も書けばよかったですね。すぐに思いつく注意として以下があります。

  • ライセンスに注意。コピペして使って良いとは限らない
  • 品質に注意。バグがあるかもしれない
  • セキュリティに注意。脆弱性があるかもしれない

 さて、先ほど引用したmakeLink()関数にはバグがあるようです。mb_eregの内部エンジンの鬼車は、Unicode文字コードの場合、[:alnum:]は半角英数字以外に、全角の英数字、平仮名、カタカナ、漢字などにもマッチします。これは恐らく意図した動作ではないでしょう。例えば、以下をご覧下さい。

var_dump(makeLink('例えばhttp://example.jp/を参照して下さい'));
【実行結果】
string(108) "例えば<a href="http://example.jp/を参照して下さい">http://example.jp/を参照して下さい</a>"

 全角の部分までリンクになっていますね。元のプログラムはURLとして許される文字だけにマッチする正規表現と思われるので、これは意図した動作ではないと思われます。[:alnum:]ではなく、[a-zA-Z0-9]などと書けばよかった。
 これに近い注意は徳丸本のP83に、コラム「mb_eregの\dや\wに注意」として書いてありますので参照ください。

4.3.1 クロスサイト・スクリプティング(基本編)

 クロスサイト・スクリプティング(XSS)については、かなり気をつかっているように見受けられますが、それでももれはありました(P98のsample18.php)。これは絵に描いたようなXSSですね。

if (isset($_POST['my_id'])) {
    $_SESSION['my_id'] = $_POST['my_id'];
}
// 中略
<p>ようこそ<?php echo $_SESSION['my_id']; ?>さん</p>

 ちなみに、htmlspecialcharsの説明は、同書P47に既出です。

4.4.1 SQLインジェクション

 私が見た範囲ではSQLインジェクション脆弱性はありませんでしたが、気になる箇所はありました。
 まず、プレースホルダは用いておらず、mysql_real_escape_stringによるエスケープを採用しています。プレースホルダを使う方が望ましいと考えます(参考→Webアプリケーションとかの入門本みたいのを書く人への心からのお願い)。
 また、文字エンコーディングの指定に、以下を使っています。

mysql_query('SET NAMES UTF8');

 これはまずいとされていて、mysql_set_charset関数を使うべきです。詳しくは、千葉さんの素晴らしいエントリ「libmysqlclientを使うプログラムはset namesをutf8であっても使ってはいけない | へぼい日記」を参照下さい。
 もう一つ、数値型の扱いです。これは典型例で説明します(同書P253)。

$sql = sprintf('SELECT * FROM members WHERE id=%d',
    mysql_real_escape_string($_SESSION['id'])
);

 列idは数値型であり引用符で囲まれていません。これは正しいのですが、それにも関わらずmysql_real_escape_stringでエスケープしています。このエスケープは意味がありません。エスケープというのは、文字列リテラル中でのみ意味があるからです。このあたりの詳しくは、「安全なSQLの呼び出し方」を参照下さい。
 sprintfの書式%dを使っている時点で整数しか出力されない(すなわちSQLインジェクションは発生しない)ことが確実ですが、さらに整数へのキャストをするとよいでしょう。%d書式で受けるということは、整数型を期待しているわけですし、ソース上もSQLインジェクション対策が行われていることが一目でわかるようになり、チェック等が容易になります。

$sql = sprintf('SELECT * FROM members WHERE id=%d',
    (int)$_SESSION['id']);

4.5.1 クロスサイト・リクエストフォージェリ(CSRF

 「ひとこと掲示板」の投稿画面にCSRF脆弱性があります。

4.6.4 セッションIDの固定化

 ログイン時にセッションIDを変更していないので、セッションIDの固定化の脆弱性があります(同書P249)。

4.7 リダイレクト処理にまつわる脆弱性

 脆弱性ではありませんが、同書P89の以下の記述は誤りです。

(引用者注:リダイレクトの)移動先のURLには、「http://」から始まる他のサイトのURLや「../」などから始まる相対パス、「/」から始まるルート相対パス」などが利用できます。

 HTTP/1.1の規格RFC261RFC2616では、Locationヘッダの指定するURLは絶対URLであることを要求しています(徳丸本P191脚注3)。
 また、狭義の脆弱性ではありませんが、リダイレクト後にただちに終了せず、後続の処理が動いている箇所があります。例えば、前述のログイン処理で、header関数実行後にexit();していないので、ログインフォームが出力されています(ブラウザには表示されません)。これはまぁバグですね。

追記(2011/09/09)

 ログインしていない場合にheader関数実行後終了せずに後続処理が動いている箇所があります。例えば、以下は、ログインしていない場合でも、投稿処理になだれ込んでいます。

} else {
    // ログインしていない
    header('Location: login.php');
}

// 投稿を記録する
if (!empty($_POST)) {
    if ($_POST['message'] != '') {
        $sql = sprintf('INSERT INTO posts SET member_id=%d, message="%s", reply_post_id=%d, created=NOW()',
// 後略

 こちらは、はっきりした脆弱性ですね。

4.8.1 クッキーの不適切な利用

 クッキーの不適切な利用がありますが、5.1.4自動ログインの項で説明します。

4.12.3 ファイルダウンロードによるクロスサイト・スクリプティング

 画像のアップローダがあり、XSSスクリプトインジェクションの問題を避けるために、拡張子をチェックしていますが、この処理にバグがあります。該当箇所(同書P110)を引用します。

$file = $_FILES['my_img'];
// 中略
$ext = substr($file['name'], -3);
if ($ext == 'gif' || $ext == 'jpg' || $ext == 'png') {
    $filePath = './user_img/' . $file['name'];
    move_uploaded_file($file['tmp_name'], $filePath);

 ご覧のようにファイル名の後ろ3文字を確認して、「gif」などであればOKとしていますが、この処理にバグがあります。拡張子がなくファイル名の末尾が「gif」などで終わっている場合(例:agifというファイル名)も許可されることになります。同様の問題が「ひとこと掲示板」にもあります(同書P239)。
 ファイル名に拡張子がなく、中味がHTMLである場合、IEではXSSが可能となります。詳しくは、徳丸本4.12節を参照下さい。

5.1.3 パスワードの保存方法

 パスワードはSHA-1ハッシュの形で保存されています。以下にログイン用のSQLの組み立て部分です(P249)。

$sql = sprintf('SELECT * FROM members WHERE email="%s" AND password="%s"',
    mysql_real_escape_string($_POST['email']),
    sha1(mysql_real_escape_string($_POST['password']))
);

 2つの問題があります。まず、ソルトなしのSHA-1ハッシュでは、パスワード保護としてはあまり意味がありません。レインボーテーブルなどで元パスワードが解読できるからです。ソルトと、できればストレッチングをする必要があります。詳しくは、徳丸本P326以降を参照下さい。
 第2に実装が変です。先の実装では、パスワードをSQL文字列としてエスケープしてからハッシュ値を求めていますが、まずハッシュ値を求めるべきです。sha1関数の結果は16進文字列ですからエスケープの必然性はありませんが、統一的にエスケープすることが望ましいでしょう。ということで、2つの関数の適用順序が逆です。

5.1.4 自動ログイン

 「ひとこと掲示板」には自動ログインの機能が用意されていますが、実装方法としてログインID(メールアドレス)とバスワードを平文でCookieに保存するという方法が用いられています(同書P250)。
 この方法は、徳丸本P330の「(自動ログインの)危険な実装例」として紹介されている方法です。また、「よくわかる」のP96には以下のように記述されています。

本文では、ログインIDのみをCookieに保存しました。Cookieには、いくつでも値を保存できるので、ログインパスワードも記憶できた方が、次回から自動的にログインできて便利になるでしょう。
しかし、Cookieはブラウザの簡単な操作で内容を見ることができてしまううえ、これまでも悪意のあるプログラムによって、盗まれるといった被害が絶えず、安全に保管しておくことはできません。
そのため、Cookieには重要な情報を保存しておくべきではありません。【後略】

 引用部分に同意します。著者がこの方針を貫かなかったことが残念です。

5.1.7 ログアウト機能

 ログアウト処理では、GETメソッドを使っていることと、CSRF対策が抜けているところが気になったところですが、大きな問題ではありません。POSTメソッドを使った方がよい理由と、CSRF対策した方が良い理由は徳丸本を参照下さい。

5.2.1 ユーザ登録

 「ひとこと掲示板」には同一ログインID(メールアドレス)の重複確認をしていますが、この処理が不十分で、重複したアカウントができてしまいます。
 同書で採用している方法は以下の通りです。会員登録のフォームは、入力-確認-登録の3画面で構成されています。重複チェックは確認画面で処理されるので、ほぼ同時に同一ログインIDの登録が要求されると、重複のチェックができません。ほぼ同時といっても数秒程度のずれであれば、二重登録されるとみてよいでしょう。
 この問題は、徳丸本P347に「■ユーザIDの重複防止」として説明しています。また、同じページに「◆事例2:ユーザIDに一意制約をつけられないサイト」にも該当しています。「よくわかる」の場合は、一意制約をつけられない理由はなく、どうして一意制約をつけなかったのかは疑問です。

まとめ

 『よくわかるPHPの教科書』のセキュリティ上の問題について報告しました。今まで、PHPの書籍を何冊か読んできましたが、入門者がセキュリティも含めて学べる良書がないのが現状です。これまで読んだ本の中では、「PHP 逆引きレシピ (PROGRAMMER'S RECiPE)」と「パーフェクトPHP (PERFECT SERIES 3)」は比較的セキュリティに気を配って書いてありましたが、完全というわけではなく、また本当の入門者が学ぶのによいという性格の本でもなさそうです。
 このため、「よくわかる」などでPHPを学ばれた方は、次にはぜひ拙著「体系的に学ぶ 安全なWebアプリケーションの作り方」でセキュリティ面の強化をされるとよろしいかと思います(宣伝)。
 また、PHPアプリケーションのセキュリティについては、PHPカンファレンス2011にて「徳丸本に学ぶ 安全なPHPアプリ開発の鉄則2011」という演題で講演することになりましたので、こちらもあわせてよろしくお願い致します。

[PR]