2015年5月2日土曜日

エラーメッセージによるXSSにご用心

以前の記事「PHPのdisplay_errorsが有効だとカジュアルにXSS脆弱性が入り込む」では、php.ini等でdisplay_errorsを有効にしていると、スクリプトに脆弱性がなくてもXSS(クロスサイトスクリプティング)脆弱性が入り込む可能性が高いことを指摘しました。
しかし、display_errorを無効にしていても、エラー処理がまずいと、エラー表示が原因でXSS脆弱性が入り込む場合があります。ネット上のサンプルスクリプトを見ても、潜在的にXSS脆弱性があるものが多くあります。

サンプルスクリプト

まずは、典型的な脆弱性の例をスクリプトで紹介します。PHP+PDO+PostgreSQLの組み合わせです。
<?php
try {
  $db = new PDO("pgsql:host=localhost options='--client_encoding=UTF8';dbname=test",
    DBUSER, DBPASS);
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 例外を有効に設定
  $ps = $db->prepare("INSERT INTO users VALUES(?, ?)");
  $ps->bindValue(1, $_GET['id'], PDO::PARAM_INT);
  $ps->bindValue(2, $_GET['name'], PDO::PARAM_STR);
  $ps->execute();
  echo '登録しました';
} catch (PDOException $e) {
  die($e->getMessage());
}
ご覧のように、PostgreSQLのデータベースに接続して、id(整数)とname(文字列)を挿入するスクリプトです。

エラーの表示例

エラー表示の例を示します。下記は、整数列であるidに「xxx」を指定した場合の画面表示です。「 invalid input syntax for integer: "xxx"」と表示されていますが、入力値が「そのまま」表示されているところに嫌な予感がしますね。


XSS

下記は、idにJavaScriptを指定した場合の画面表示です。ご覧のように、JavaScriptが起動しています。XSS脆弱性が混入してしまいました。


この際のHTMLソースは以下の通りです。
SQLSTATE[22P02]: Invalid text representation: 7 ERROR:  invalid input syntax for integer: "<script>alert(1)</script>"
エラーメッセージをHTMLエスケープしないで表示していることが原因です。

対策

本来、システム内部的なエラー内容は画面に表示すべきではありません。利用者にとっては不必要な情報であり、かつ攻撃のヒントが得られる場合があるからです。このため、エラーの詳細情報は画面表示せず、ログファイルに出力するようにします。
一方、なんからの事情があってエラー情報を画面表示する場合は、表示の前にHTMLエスケープすべきです。
例: echo htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');

まとめ

エラーメッセージに起因するXSSについて説明しました。
Google検索で「PDOException getMessage」で検索すると、エラー詳細をHTMLエスケープしないで表示しているサンプルが大半です。学習目的あるいはデバッグ用という意図かもしれませんが、サンプルには明示されていないため、初学者はそれが「正しい書き方」と思うのではないでしょうか。
そもそもエラー詳細を画面表示しないことが原則ですが、説明等の目的でエラー内容を表示する場合は、常に正しいスクリプトを示すという意味からHTMLエスケープを忘れないようにいたしましょう。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年4月30日木曜日

Apacheの多重拡張子にご用心

先日の日記『「10日でおぼえるPHP入門教室 第4版」はセキュリティ面で高評価』では、同書のアップロード機能のセキュリティ面を評価しつつ、「もうひと踏ん張り確認して欲しい内容がある」として、画像XSSの可能性について指摘しました。では、これを直せば完璧かというと、実はそうとも言えないという微妙な問題があります。それは、アップロード先の場所とファイル名の問題です。
ファイルをアップロードするディレクトリ: ドキュメントルート下の /php10/doc/
ファイル名: ブラウザから送信されたファイル名そのまま
これらのうちファイル名の拡張子については、gif/jpg/jpeg/pngのみを許すという、いわゆるホワイトリスト検査がされていて、またgetimagesize()関数により、画像ファイルであることの簡易的なチェックをしています。しかし、この状態では、環境によってはアップロードしたファイルをPHPスクリプトとして実行される危険性があります。

同書は、その種の危険性を考慮して、以下の様な注意書きを書いています(同書P214)。
COLUMN: ファイル名の保存先には要注意

同書では、サンプル実行の簡単さを優先して、ドキュメントルートの配下にアップロードファイルを保存しています。しかし、これは不正なファイルをアップロードされたときに、そのまま実行されてしまう危険もはらんでいます。
一般的には、アップロードファイルは、ユーザーが直接アクセス出来ない--ドキュメントルートの外に保存するようにしてください。
これは実に正しい指摘ですが、それでは拡張子のホワイトリストチェックをしても、どうしてスクリプト実行されるかという疑問が生じます。その方法の一つが、Apacheの多重拡張子を悪用するという方法です。

多重拡張子

Apacheは元々の仕様として多重拡張子を扱えるようになっていて、その代表例はApache自身のマニュアルファイルに見ることができます。Apacheのマニュアルファイルには、以下のように各言語用のコンテンツが並んでいます。
index.html.en
index.html.fr
index.html.ja.utf8
最後の例だと、htmlに加えて、ja(言語=日本語)とutf8(文字エンコーディング=UTF-8)という具合に三種類の拡張子がついています。
これは、Apacheのディレクティブの中に拡張子によって挙動を変えるものが複数存在するためにおこる問題です。

ファイル名の途中にあるphp拡張子でスクリプトが起動する場合

このため、設定にもよりますが、phpinfo.php.png等ファイル名の末尾以外でも.php.が含まれていると、ApacheがこのファイルをPHPスクリプトとして判断してスクリプトを起動してしまう場合があります。具体的には、AddHandlerにより以下のようにPHPスクリプトを設定している場合が該当します。
AddHandler php5-script .php
そのような環境がどれくらいあるかは分かりませんが、メジャーなところではRHEL/CentOSが該当するので、影響は広範囲に渡りそうです。

「10日でおぼえるPHP入門教室 第4版」の環境は該当しない

それでは、「10日でおぼえるPHP入門教室 第4版」の環境(XAMPP)はどうかというと、PHPのApache設定は下記の通りで、この場合は上記の問題に該当しません。
#
# PHP-Module setup
#
LoadFile "C:/xampp/php/php5ts.dll"
LoadModule php5_module "C:/xampp/php/php5apache2_4.dll"

<FilesMatch "\.php$">
  SetHandler application/x-httpd-php
</FilesMatch>
従って、「書籍に書いてある通りに動かす分には脆弱性ではない」ということになります。冒頭で「微妙な問題」と書いた理由はこれです。

同書のスクリプトをCentOS上で動かすとスクリプト実行の脆弱性が発現する

しかし、同書でPHPを勉強した読者が、レンタルサーバー等を借りて自作のスクリプトを動かすと、脆弱性が悪用される危険性が生じます。
検証に必要な画像ファイルの作り方は、以前のエントリと同様で、JavaScriptの代わりにPHPスクリプトを書くだけです。そのようにして作成したファイルをアップロードしてブラウザからアクセスした様子を示します。実験にはCentOS6を用いました。


対策

対策には複数の種類があります。いずれか一つで対策になりますが、適宜複数を組み合わせることで安全性が高まります。推奨は (3)と(4)の組み合わせです。

(1)Apache側の設定を変える
XAMPPで用いている方法でPHPスクリプトを動かすようにする方法です。この場合は、ファイル名末尾が .php の場合のみPHPスクリプトとして動作するので、アプリケーション側の拡張子チェックをすり抜けることはありません。

(2)ファイルのアップロードディレクトリでPHPが動かないようにする
アップロードファイルがドキュメントルート配下に置かれる場合、該当のディレクトリでPHPが動かないようにする方法があります。httpd.confや.htaccessにて以下の設定を指定します。
RemoveHandler .php
この方法を推奨するというわけではありませんが、既存のアプリケーションで緊急対処が必要な場合には役立つでしょう。設定後、無害な test.php.png などのスクリプトでPHPとして動かないことを検査することと、検査後はそのファイルを必ず削除することを忘れないで下さい。また、php以外の拡張子を環境に応じて設定して下さい。

(3)アップロードファイルをドキュメントルート配下に置かない
この方法を強く推奨します。同書注意書きにもあるとおりですし、拙著『体系的に学ぶ 安全なWebアプリケーションの作り方 』でもこの方法を推奨しています。

(4)ファイル名を付け替える
そもそもブラウザから送信されたファイル名をそのまま使ってファイルを保存するという仕様はよくありません。ファイル名の衝突がありえるからです。ファイル名は他と衝突しないユニークなものを採番してつけるようにしましょう。これも拙著で推奨している方法です。これは、セキュリティ要件以前のアプリケーション要件として必要ですね。

まとめ

Apacheの多重拡張子の危険性と対策について説明しました。Apacheの多重拡張子は脆弱性ではなく仕様ですので、できればアプリケーション側で対処した方がよいと私は考えます。対策として説明した(3)と(4)は、Apache以外の環境でも安全性に寄与するため、強く推奨する方法です。
ファイルのアップロードはPHPから簡単にできてしまうので、PHP入門書でもしばしば取り上げられていますが、セキュリティの面からは危険な実装になっている場合が多いです。インターネットに公開するためには、セキュリティを強化する必要があります。
入門書だからそれでも仕方ないではないかという意見もあるでしょうが、私はそうは思いません。多くの読者は、それが危険な方法と知らないままに、その方法でインターネット上のサイトを作り続けるでしょう。最初から安全な方法を紹介するか、ファイルアップロード機能の紹介を諦めるか、どちらかです。つまり、ファイルのアップロードは、実は簡単ではないととらえるべきだと思います。

参考



【HASHコンサルティング広告】
この記事はHASHコンサルティング勤務中に書かれた。
HASHコンサルティング株式会社は、本物のセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年4月28日火曜日

みずほ銀行のトランザクション認証を試してみた

既に報道されているように、インターネットバンキングに対する不正送金事件が多発しています。
警察庁は2015年2月12日、2014年(平成26年)の1年間に発生した、インターネットバンキングでの不正送金事件の被害状況などに関するデータを発表した。

 不正送金事件の発生件数は1876件となり、前年の1315件から500件以上増加。被害額については約29億1000万円となり、前年の14億600万円から2倍超の増加となった。
2014年のネットバンキング不正送金は約29億円で法人被害が激増、警察庁発表 より引用
このような状況を受けて、フィッシング対策協議会では、インターネットバンキングの不正送金にあわないためのガイドラインとして以下の「鉄則」を公開しています。
  • 第一の鉄則:乱数表等(第二認証情報)の入力は慎重に!
  • 第二の鉄則:インターネット利用機器を最新の状態に保とう!
これらは確かに重要な施策ですが、現在多発していると言われるwebinject攻撃や、近い将来日本でも悪用されると予想されるMITB(Man in the Browser)攻撃に対しては十分とは言いにくいと考えます。

webinject攻撃や、MITB攻撃については以下を参照ください。
簡単に整理すると、これらはどちらもマルウェアがブラウザを操作するタイプの攻撃ですが、webinjectはログイン時等にニセの画面を表示して、乱数表の内容やワンタイムパスワード(OTP)を入力させるもの、MITBは利用者が振込操作をする際に、振込先や金額をマルウェアが変更するものです。

webinject攻撃は、利用者にOTPを入力するように促すため、利用者がサイトにログインする度に被害の可能性がある一方で、「このタイミングでOTPを入力するのはおかしい」と、利用者が気づく機会があります。一方MITBは利用者がOTPを入力するのをマルウェアがじっと待っています。このため、利用者が振込操作等をしない限り被害にあうことはありませんが、その代わり、利用者が不正送金に気づくことは不可能ということになります。

このような状況を受けて、みずほ銀行は、3月下旬より、トランザクション認証の提供を開始しました。
「トランザクション認証」機能とは、強固なセキュリティ対策の一つで、「登録先以外へのお振込」時に使用します。
「登録先以外へのお振込」時、お手元のワンタイムパスワードカードにお振込先の口座番号をご入力いただきワンタイムパスワードを発行します。このワンタイムパスワードとお振込先口座番号を紐づけ、お客さまが指定した振込先であるか確認します。
これにより、近年確認されている、悪意のある第三者がお振込先口座の情報を勝手に書き換え、お客さまが意図しない口座に振り込むという犯罪を、防止することができます。
みずほ銀行:ワンタイムパスワードカード より引用
従来みずほ銀行の口座は持っていなかったので、トランザクション認証を試すために口座開設して試してみましたので報告します。

トランザクション認証を使ってみる

自分のみずほ銀行の口座から、おなじく自分の三井住友銀行の口座に1万円振り込んでみます。まずは通常通り振込先と振込金額を指定すると以下の画面が表示されます。


この状態で、ワンタイムパスワードカードの ③ キーを押すと、以下のように入力が可能なモードに変わります。

ここで、「振込先の」口座番号7桁を入力します。以下の「9876543」は架空の口座番号です。


入力が終わったら右下の「OK」ボタンを押すと、以下のように 8桁のトークンが表示されます。


この 8桁トークンをウェブ画面の「ワンタイムパスワード」欄に入力して、「振込実行」ボタンをクリックしすると、振込が実行されます。
利用者が入力したトークンは、カード上で入力した口座番号にのみ有効なので、仮にMITB攻撃でトークンが窃取されたとしても、(たまたま口座番号が一致しない限り)攻撃者の口座に不正送金されることはありません。
この後、振込先口座を登録することができ、一端登録した口座は、次回からは「信頼された口座」として、トランザクション認証なしに振込が可能になります。この口座登録は、(1)銀行窓口で登録、(2)振込操作の後に登録、の二種類でしかできないので、攻撃者が自分の口座をまず登録するという手口はできないと思われます。

残る脅威

webinject攻撃等で、マルウェアが画面を操作して、利用者にトランザクション認証カードの操作を指示し、「攻撃者の口座番号」をカードに入力するように誘導する攻撃の可能性がある、と高木浩光氏が指摘しています。
高木氏の指摘は、三井住友銀行やゆうちょ銀行のように、「トランザクション認証機能つきカードを配ったが、現在は単純なワンタイムパスワードとして用いていて、将来トランザクション認証方式に変更する」場合の運用を危惧したものですが、みずほ銀行の場合は、最初からトランザクション認証用としてカードを配っているので、被害にあう可能性は相対的に低くなるでしょう。
とはいえ、やはり騙される人はいると予想しますが、カードに同封された説明書には、以下のように注意がありました。素晴らしいですね。


ここまでしても騙される人はいるでしょうが、まずは、みずほ銀行の努力と気配りを賞賛したいと思います。

まとめ・感想

みずほ銀行が「日本で最初に導入」したトランザクション認証を試用してみました。
不正送金の手口はどんどん進歩していて、利用者側の注意だけでは被害を防ぐことは難しい状況になっています。このため、本格的なMITB防止機能としてトランザクション認証機能を提供したみずほ銀行の英断は素晴らしいと思います。
とはいえ、前述したように、それでもダマされる手口は残る可能性はあることと、そもそも操作が難しくて普及が難しそうだと感じました。
元々はトランザクション署名の試験用に口座を開設したみずほ銀行の口座ですが、月に4回までは振込料金が無料ということを知り、本来の銀行口座としても有効活用していこうと思います:)

追記(17:15)

何気なくパスワードカードの裏を見ると下図のようになっていました。


以下の文言が気になりました。
※登録先以外へのお振込み時には振り込み画面上の【手順】に従ってお手続きください。
これはいただけない。振込画面はマルウェアによって改ざんされている可能性があるからこそのトランザクション認証なので、画面通りに操作すると不正送金の被害にあう可能性があります。この文言は以下のようにすべきところです。
※登録先以外へのお振込み時には、カード同梱の説明書の【手順】に従ってお手続きください。
セキュリティって難しいですね。


【HASHコンサルティング広告】
この記事はHASHコンサルティング勤務中に書かれた。
HASHコンサルティング株式会社は、本物のセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。

2015年4月24日金曜日

「10日でおぼえるPHP入門教室 第4版」はセキュリティ面で高評価

弊社本社の麻布十番移転に伴い、本社近くの麻布図書館を利用しています。麻布図書館は土地柄のイメージにあう瀟洒な建物で、蔵書がない場合は港区の他の図書館から取り寄せ(無料です)ができますので、よく利用しています。今回は、山田祥寛さんの「10日でおぼえるPHP入門教室 第4版 」を借りて読んでみました。一読して、本書がセキュリティにもよく配慮されていることがわかりましたので、以下にご紹介したいと思います。

クロスサイトスクリプティング(XSS)

表示の際にHTMLエスケープするという原則を忠実に守っています。そのため、下記の e() という関数を定義して呼び出しています。
function e($str, $charset = 'UTF-8') {
  return htmlspecialchars($str, ENT_QUOTES, $charset);
}
その他にもXSS対策として重要な下記の内容を本文でもしっかり説明しています。XSSのためという文脈ではありませんが、むしろXSS対策ではなく自然な形で以下を実践することが重要です。
  • 文字コードの指定(php.iniのdefault_charset指定、meta要素による指定の両方)
  • 属性値をダブルクォートで囲む
したがって、入門レベルのXSS対策としては満点と言ってよいでしょう。PHP入門書のイケテナイXSS対策を見続けてきた身としては、上記は感涙ものです。

SQLインジェクション

プレースホルダの使用を徹底することによりSQLインジェクション対策をしています。大きな隙はありません。ライブラリとしてはPDOを使い、以下のスクリプトで接続しています。
$db = new PDO('mysql:host=localhost;dbname=php10;charset=utf8', 'phpusr', 'phppass');
文字エンコーディング指定もしっかりしていて素晴らしいです。ただし、欲を言えば以下を指定しているともっとよかったでしょう。
  • $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // 静的プレースホルダを指定
  • $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //: 例外を投げる
本書では try ~ catchは使用していますが、PDO::ERRMODE_EXCEPTIONを指定していないと、接続時以外では例外が発生しません。すなわち、エラーをうまく捕捉できなくなります。


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

PHPの入門書では、ほぼ例外なくCSRFの解説がありませんが、本書ではしっかりCSRFの解説があります。素晴らしいですね。
ただし、その実装は少々残念な感じです。CSRF対策の手法としてワンタイムトークンを使っているのですが、トークン生成のアルゴリズムが暗号学的に弱いのです。具体的には以下のスクリプトです。
$token = md5(uniqid(mt_rand(), TRUE));
mt_rand()は暗号学的な擬似乱数生成器ではないので、厳密に言うと、上記で生成するトークンには予測可能性があるということになります。
「そんな馬鹿な」と思う読者もいるでしょうが、上記と似て非なる方法で生成されるPHPのセッションIDの予測可能性について検証した論文があり、hnwさんの素晴らしい翻訳で読むことができます。
セッションIDはシードとしてIPアドレスと時刻を用います。上記のmt_randの初期シードはプロセスIDと時刻であり、プロセスIDの変化にはかなり規則性があります。擬似乱数生成器として、セッションID生成のほうがLCG(線形合同法)に対して、こちらのトークンはメルセンヌ・ツイスタですが、予測困難でないことは両者に共通しています。つまり、この程度の仕組みだと、予測困難とはいえないということです。
ではどうすればよいかですが、PHP5.3.0以降を使う前提であれば、openssl_random_pseudo_bytes() 関数を使えばよいでしょう。

なお、ワンタイムトークンにしたことにより、ワンタイムでないセッション限りのトークンと比べて、トークンの予測は容易になってしまいます。これに関しては、以前の記事「CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例」を参照ください。

パストラバーサル

パストラバーサル(ディレクトリトラバーサル)脆弱性の解説が同書P231にあります。対策方法は、対象ファイルが保存されているディレクトリからファイル一覧を取得し、外部から指定したファイル名がその中に存在するかを確認するという独特な方法です。対策にはなりますが、ファイル数が多い場合に効率が悪そうですね。basename()関数を使うという標準的な方法を解説してくれたらもっと良かったのにと思いました。

メールヘッダインジェクション

本書には、なんとメールヘッダインジェクションの説明もあります(P169)。素晴らしいですねぇ。なまじっかなセキュリティ解説書にもメールヘッダインジェクションがない場合も多いのに…
対策としては、外部から指定するメールアドレスをバリデーションするというオーソドックスな方法です。

セッションフィクセーション

セッションフィクセーションについて明示的な説明はありませんが、本書では認証にPear::Authを用いていて、Pear::Authの内部でセッションフィクセーション対策が取られています。

ファイルアップロード

PHP入門書の多くでファイルアップロード機能を扱っており、本書も例外ではありませんが、ファイルアップロード機能には脆弱性が入りやすくので気をつける必要があります。本書は、かなり注意深い実装がされています。
まず、拡張子のチェックです。これに関して以下の説明があります。
例えば、有害なスクリプト(.phpファイル)をアップロードされてしまえば、自分のサーバーを踏み台に好き勝手な処理を実行されてしまう恐れもあります。最低でも、アップロード可能なファイルの拡張子は制限しておくべきです。
加えて、getimagesize()関数により、アップロードされたファイルの中身が画像であることを確認しています…が、セキュリティ上の効果はあまり期待できません。セキュリティ目的ではなく、画像であることを少しでも確実にするというチェックなのかもしれません。
実は、もうひと踏ん張り確認して欲しい内容があるのですが、これについては後述します。

残念な点

先に紹介したワンタイムトークンの生成アルゴリズムに加えて、以下の様な「残念な点」があります。重大な脆弱性というわけではありませんが、他がよく書けているだけに余計に残念な感じがしました。

IE7で画像XSSが可能

Internet Explorer7(IE7)以前では、画像を用いたクロスサイトスクリプティング攻撃が可能です。原理と対策の方針については下記の記事を御覧ください。
攻撃のために用いる画像は以下のようにして作成可能です。
  1. 適当な.png画像を用意する
  2. IDATの中身にJavaScriptを埋め込む
  3. 拡張子をgif(あるいはjpg)に変更する
作成した画像の例を下記に示します。


これをアップロードしてIE7で表示させると下記の結果になります。


これに該当するブラウザは、現在サポートが有効なもの(サーバー除く)ではWindows Vista上のIE7だけで、2016年1月でサポートが終了します。対策するかどうか迷うラインではありますが、幸いgetimagesize()関数を用いて比較的容易に対策できます。具体的には、
  • getimagesize()関数を用いてマジックバイトから画像のタイプを取得する(参考
  • ファイル名から得た拡張子が、画像のタイプと一致していることを確認する
  • 両者が一致していな場合はエラーとして画像を受け付けない
詳しい説明や具体的なコードは拙著P277以降を参照ください。

パスワードの保存形式がソルトなしMD5ハッシュ値

前述のように、本書の認証機能はPear::Authを用いており、そのデフォルトの挙動として、パスワードはMD5ハッシュ値として保存されます。現在の状況として、ソルトなしのMD5では安全な保存とはいえません(下記の記事参照)。
PHP5.5以降が使える環境ではpassword_hash()関数を使いたいところですが、Pear::Authとは共存できないと思われるので、これは本書のカバー範囲を超える「ぜいたくな要求」かと思います。しかし、「本当はこれではダメなんだ」ということは言及していただきたいです。

まとめ

「10日でおぼえるPHP入門教室 第4版」について、脆弱性対処の状況を報告しました。一部残念な点はあるものの、全体としてはPHP入門書の中ではセキュリティの説明が非常に充実していると感じました。要点を以下にまとめます。
  • SQLインジェクション対策はプレースホルダ、PDOの文字エンコーディング指定で確実に
  • XSS対策は、表示の際のエスケープを徹底
  • CSRF対策はワンタイムトークンを用いる
  • パストラバーサル対策、メールヘッダインジェクション対策もある
前回のエントリのまとめで私は以下のように書きました。
しかし、せっかくPHPを勉強するのであれば、最初から正しい方法を学びたいものです。ということで、PHPの入門書を書く方には、「出力の際にHTMLエスケープ」を徹底していただきたいと希望します。
そして、それが十分に実践されている本書を読んで、私は上機嫌になりました。


2015年4月21日火曜日

PHP入門書のSQLインジェクションとXSS対策をあらためて調べてみた

継続的にPHP入門書のセキュリティ問題を確認していますが、今回は「やさしいPHP 第3版」を取り上げ、今どきのPHP入門書のセキュリティ状況を報告したいと思います。
上記のように、2008年に初版が出版された後2回の改版がありました。
第2版ではクロスサイトスクリプティング(XSS)の説明が追加され、第3版ではXSSに加えSQLインジェクションの説明が追加されました。つまり、初版ではこれらの説明はなかったということです。

第3版におけるSQLインジェクションの対策方法はプレースホルダによるもので、結果として本書にSQLインジェクション脆弱性は見当たりません(パチパチパチ)。本書にはSQLの(文字列リテラルの)エスケープに関する説明はありませんが、これは、「PHPとセキュリティの解説書12種類を読んでSQLエスケープの解説状況を調べてみた」で報告したように最近のPHPの解説書のトレンドで、私の記事にも書いたようにPHP入門書レベルならそれで十分かと思います。最初のうちは、安全なプログラムを自然な形で書けることが重要だと思うからです。 ということで、以下を宣言させてもらいたいと考えます。
PHPの入門書ではSQLインジェクション脆弱性がないことが当たり前になった
一方、XSSはどうでしょうか。本書第3版では、P362に以下の説明があります。
フォームからの入力情報に注意する
Webアプリケーションでは、セキュリティに注意しなければなりません。特にフォームから入力する場合には注意すべきことが多くあります
この後、XSSの攻撃例やHTMLエスケープの方法などが続きます…が、「フォームからの入力情報に注意」という表現にとても嫌な予感がしますね。「フォームからの入力」以外はどうなっているかと調べてみると、同書P400のSample8.phpにあるように、入力値以外にはHTMLエスケープ処理が入っていません。以下に引用しますが、入力した内容のデータベースへの登録と、データベースの内容の表示処理があります。
// 以下、データの追加
if(isset($_POST["insert"])){
  $ipd = $_POST["insproduct"];
  $ipc = $_POST["insprice"];
  $qry ="INSERT INTO product(name, price) VALUES(:ipd, :ipc)";
  $stmt = $db->prepare($qry);
  $stmt->bindParam(":ipd", $ipd);
  $stmt->bindParam(":ipc", $ipc);
  $stmt->execute();
}

// 中略

$qry = "SELECT * FROM product";
$data = $db->query($qry);

// 中略。以下、データの表示

while($value = $data->fetch()){
  $id = $value["id"];
  $name = $value["name"];
  $price = $value["price"];
  print "<tr><td><input type=\"radio\" name=\"delid\" value=\"{$id}\"/></td>
             <td>{$id}</td><td>{$name}</td><td>{$price}</td></tr>\n";
}
フォームから入力された文字列がデータベースに登録され、それが表示される過程でHTMLエスケープは行われていません(上記赤字部分参照)。その結果、クロスサイトスクリプティング脆弱性が存在します。以下の文字列を製品名の欄に入力してやると…
<script>alert(1)</script>
下記のようにJavaScriptが実行されます。



ところで、この画面表示、よく見ると文字が化けています。調べてみると、本来UTF-8でレスポンスが送信されているのに、レスポンスに文字エンコーディングの指定がないので、ブラウザにShift_JISとみなされ、文字化けの原因になっています。これはXSSの原因になりえます。また、そもそも文字化けが起こるということは、アプリケーションとして不完全な状態です。
PHPの場合、HTTPレスポンスの文字エンコーディングを指定する方法には以下の3種類がありますが本書では特に説明されていません。
  • header関数で指定する
  • php.iniのdefault_charsetで指定する
  • HTML内に指定する
header関数で指定する
header関数によりContent-Typeヘッダを出力してそこでcharsetを指定する方法がもっとも確実です。
header('Content-Type: text/html; charset=UTF-8');

php.iniのdefault_charsetで指定する
php.iniにおいて以下の指定を有効にすることでも、同様の指定ができます。PHP-5.6.0以降ではこれがデフォルトになりました。
default_charset = "UTF-8"

HTML内に指定する
HTMLのhead要素内で以下を指定することでもcharsetを指定できます。php.iniの変更ができない環境等では便利です。
<meta charset="UTF-8">

結局XSS対策として最低限説明すべきことは何か?

今まで説明したように、「やさしいPHP」のXSS対策の解説は不十分です。それではどう書けばよいかですが、IPA「安全なウェブサイトの作り方 改訂 第7版」には、以下の説明があります(抜粋)。
5-(i) ウェブページに出力する全ての要素に対して、エスケープ処理を施す。
   解説内:また、HTMLタグを出力する場合は、その属性値を必ず「"」(ダブルクォート)で括る
5-(viii) HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する。
本書の場合、これらのいずれもが十分ではありません。

まとめ

書籍「やさしいPHP 第3版」のSQLインジェクションとXSSの解説状況について報告しました。SQLインジェクションに関してはプレースホルダの使用により脆弱性や説明の誤りはありませんでした(パチパチパチ)。一方、XSSに関しては、htmlspecialcharsを用いてHTMLエスケープを行うという概要の説明は正しかったものの、その適用が十分ではありませんでした。
実はこの問題は多くのPHP入門書に共通する問題です。対策自体は十分に行えている解説書でも、入力した文字列をまずHTMLエスケープしてから扱っている書籍も多く、「表示の際にエスケープする」という基本が実践できている書籍は、入門書にはあまりなく、中級者以上向けの書籍でないと解説されていない傾向があります。
しかし、せっかくPHPを勉強するのであれば、最初から正しい方法を学びたいものです。ということで、PHPの入門書を書く方には、「出力の際にHTMLエスケープ」を徹底していただきたいと希望します。

おまけ

今回紹介した以外に、書籍「やさしいPHP 第3版」には以下のセキュリティ上の問題があります。

ファイルアップロードのサンプルにおいて、ファイル名の拡張子チェックなどがないので、そのまま使うと任意スクリプトのアップロードと実行ができる。すなわち、スクリプトインジェクション相当の脆弱性がある(10.6)

ファイルの読み書きのサンプルにディレクトリトラバーサル脆弱性がある。とくに、ファイルへの書き込みのサンプルが深刻で、任意のスクリプトをPHPファイルとして書き込めるのでスクリプトインジェクション脆弱性相当の危険性がある(12.2)。

外部コマンドの実行において、OSコマンドインジェクションの説明が一応あるが、エスケープ用関数としてescapeshellcmd()を紹介しているので、こちらで紹介しているような問題が残る(12.2)。

2015年4月16日木曜日

Time-based SQL Injectionは意外に実用的だった

このエントリでは、Time-based SQLインジェクション、すなわち時間差を利用したSQLインジェクションが意外に実用的だったという報告をします。デモ映像ありです。

はじめに

Time-based SQL Injectionという攻撃があります。これはブラインドSQLインジェクションの一種で、ある条件の場合に一定時間(例えば5秒)スリープし、そうでない時との応答時間の差で情報を盗もうというものです。1回のHTTPリクエストで1ビットの情報が得られるので、それを積み重ねることによって、いくらでも情報を盗めるはずです…理論的には。
しかし、「理屈はそうでも、時間が掛かりすぎるよね」ということで、深くは追っかけていませんでした。SQLインジェクションの検査には有効でも、悪用としての実用性はあまりないと考えていたのです。

きっかけ

きっかけは、以下のYahoo!知恵袋に以下の質問です。
SQLインジェクションについて教えて下さい
【中略】
$result = mysql_query("INSERT INTO tbl(address, mail) VALUES('$address', '$mail')", $con);
【中略】
ずばり、上記のような場合にどんな文字列でも入力可能な住所欄に何らかの文字列を入れるとSQLインジェクション攻撃は成立しますか?
http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q14140304564 より引用
ごらんのようにINSERT文の箇所にSQLインジェクション脆弱性がある場合に、どのような攻撃が成立しうるかという質問です。
私からは、ON DUPLICATE KEY UPDATE 句により、他人が登録した行を改編する攻撃を指摘したところ、「それ、副問い合わせを使えば情報を盗めるよ」と指摘をいただきました。
これで、実行されるSQL文は以下になり、mailコラムにMySQLのrootユーザのパスワードハッシュが挿入されました。
INSERT INTO tbl(address, mail) VALUES('test', (select password from mysql.user where user='root' limit 0,1)) -- -', 'dummy@example.jp')
あとは、自分のメールアドレスを表示するページで挿入した情報を表示させればOKというシナリオです。
http://blog.a-way-out.net/blog/2015/01/06/sql-injection-insert/ より引用
これは興味深い攻撃なのですが、『自分が登録した情報が自分でも閲覧できない』というサイトも多いです。たとえば、キャンペーン応募サイトなどが該当します。投稿しっぱなしで後からはその内容は閲覧できません。

一方、SQLのエラーメッセージが表示されるサイトの場合は、こちらで指摘したようにエラーメッセージから情報を漏洩させることが可能です。
問題は、どちらもできないサイトの場合です。すなわち、INSERTした情報を後から閲覧できず、SQLのエラーメッセージも表示されないキャンペーン応募サイト等において、SQLインジェクションで情報を盗む方法です。完全ブラインドとなると、Time-based SQL Injectionの出番です。

Time-based SQL Injectionの基本的な考え方

多くのSQLデータベースにおいて、一定時間スリープするという関数が用意されています。MySQLにはsleep()が、PostgreSQLにはpg_sleep()があります。これを利用して、以下のように5秒間待つという攻撃ができます。
INSERT INTO tbl(address, mail) VALUES('test', (select sleep(5))) -- ','dummy@example.jp')
これはSQLインジェクション検査で用いられる方法です。これを更にすすめて、特定条件の場合のみ5秒待つというSQL文を考えます。
INSERT INTO tbl(address, mail) VALUES('test',(select if(substr((select email from mini_bbs.members limit 0,1),1,1) = 'a',sleep(5),0))) -- ','dummy@example.jp')
これは、mini_bbs.membersテーブルのemail列、第1行1文字目が ‘a’ の場合のみ5秒待つSQL文です。これを繰り返すことにより、任意のデータを好きなだけ取り出すことができます。

高速化の試み

文字をa, b, c …と1つずつ調べるのはあまりにも不効率ということで高速化の方法を考えてみます。以前紹介したブラインドSQLインジェクションのスクリプトでは、バイナリサーチを用いて高速化を図っています。
バイナリサーチを用いると、8ビットのデータを8回程度のリクエストで特定できることになります。sleep(5)を用いる場合、1回のリクエストの平均待ち時間は2.5秒ですから、2.5秒×8で 20秒で1バイトを確定できることになります。遅いといえば遅いですが、実用にならないこともないですね。以下は、バイナリサーチを用いたSQLインジェクション攻撃(Time-based)のデモ映像です。sleep(1)を用いているので、かなり実用的です。



リニアサーチの方が速かった

ところが、その後私は勘違いをしていることに気がつきました。バイナリサーチを用いる場合、平均すると1バイトの情報を得るのに4回(8÷2)のsleepが発生します。通常のHTTPリクエストに掛かる時間を無視出来る場合(『滑車の質量は無視できるとする』と同じw)、単純なリニアサーチの方がsleepは1回だけで済むので、ずっと高速になります。以下の映像は、リニアサーチに変更したバージョンです。



文字の種類が多い場合はバイナリサーチと併用すると実用的

しかし、マルチバイトの文字を求めるような場合、リニアサーチだと数千から数十万のHTTPリクエストが必要となり、さすがにリクエストの時間を無視できなくなります。すなわち、

バイナリサーチ: O(log (N)) だが定数係数が大きい
リニアサーチ: O(N) だが定数係数が小さい

ということで、文字の種類が少ない場合はリニアサーチの方が速いものの、文字の種類が多くなってくると、バイナリサーチの方が速いという傾向があります。
実際には両者の併用が可能です。バイナリサーチである程度文字の範囲を絞っておき、その後はリニアサーチで文字を確定させるという方法です。
実は、以前紹介したブラインドSQLインジェクションのスクリプトでもこの方法を採用しているのでした。
おおまかな範囲を二分探索で絞り、そこからリニアサーチで該当文字を求めています
http://blog.tokumaru.org/2012/12/blind-sql-injection-php-exploit.html

まとめ

Time-based SQL Injection攻撃が意外に実用的であることを示しました。
結論としては、Yahoo!知恵袋でも回答した以下の内容になるかと思います。
ただし、「SQLインジェクション脆弱性はあるが攻撃はできそうもない」というケースもあり得ます。あり得ますが、思わぬ攻撃を受ける可能性もあり、その場合の被害は甚大ですので、攻撃可能性の有無に関わらず、必ずSQLインジェクション対策はしておくべきです。
そして、Time-based SQL Injectionが実用的に使えるとなると、いかなる場合でもSQLインジェクション脆弱性は許容してはならないことがはっきりした、と言えるかと思います。

2015年3月27日金曜日

キャッシュ制御不備の脆弱性にご用心

古い書籍に掲載されたPHP記述の掲示板ソフトを動かしていると、ログアウト処理がうまく動作していないことに気がつきました。チェックの方法ですが、ログアウト処理の脆弱性検査の簡単なものは、「安全なウェブサイトの作り方」別冊の「ウェブ健康診断仕様」に記載されています。
(J)認証
ログアウト機能はあるか、適切に実装されているか
ログアウト機能がない、あるいはログアウト後「戻る」ボタンでセッションを再開できる場合
この仕様書にある通り、『ログアウト後「戻る」ボタンでセッションを再開できる』状態でした。
おそらくセッション破棄がきちんと書かれていないのだろうと思いログアウト部分のソースを見ると、以下の様な処理内容でした(オリジナルからはリライトしています)。
<?php // logout.php
  require_once('common.php'); // 共通の設定・処理
  session_start();   // セッションの開始
  session_destroy(); // セッションの破棄
  header('Location: http://example.jp/');
  exit();
ソース上では、きちんとセッションが破棄されているように見えます。そこでさらに動作を確認していくと、以下が分かりました。
  • 1回目のログアウト処理はきちんと動作するが、2回目以降のログアウト処理は表示上は正しい遷移だが、セッションは破棄されていない
  • logout.phpは、ボタンではなくリンクで呼ばれている。すなわちGETメソッドで呼ばれている
2回目以降がおかしいということで、キャッシュが原因だろうということでHTTPレスポンスを確認すると、以下のヘッダが付与されています。
Cache-Control:private, max-age=10800, pre-check=10800
このヘッダは共通処理が書かれた common.php 内の以下の記述によるものでした。
session_cache_limiter('private');   // 戻るボタンを有効にする
Cache-Control: privateは、個人用のキャッシュは許すが、他人にそのキャッシュは使わせないという設定で、典型的にはブラウザのキャッシュは許すが、プロキシサーバーのキャッシュは許さないという設定です。
すなわち、logout.phpに対するリクエストは、ブラウザのキャッシュは有効であることから、2回目以降はリクエストされず、キャッシュされたレスポンスが用いられていたことになります。これが「2回目以降は見かけ上はログアウトの遷移をたどるがログアウトされていない」原因です。

対策

ログアウトのように副作用を伴う処理は、キャッシュが有効では処理が行われない場合があるので、キャッシュを無効にする必要があります。PHPの場合、以下により記述できます。ただし、PHPはデフォルトでこの設定なので、通常は明示的にこれを指定する必要はありません。
session_cache_limiter('nocache');   // キャッシュを無効にする
これにより、以下のレスポンスヘッダが送信されます。
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
また、副作用を伴う処理に対するリクエストにはPOSTメソッドを用いるべきです。ログアウト処理の場合、POSTではなくGETメソッドで呼び出している例をよく見かけますが、本来GETメソッドは副作用のないリクエストに用いるもので、副作用があるリクエストはPOST(あるいはPUT、DELETE)を用いることになっています。POSTメソッドの場合、絶対にキャッシュされないわけではありませんが、GETの場合に比べてキャッシュされにくいため、キャッシュ制御に関しても安全方向に働きます。

※注記
POSTリクエストがキャッシュされる例については、こちらを参照。また、ジャパンネット銀行のバンキングサイトは全ページPOSTメソッドという珍しい作り(セッションIDをPOSTで送っている)ですが、戻るボタンが使え、その場合はPOSTに対するキャッシュが表示されるようです。

キャッシュからの情報漏洩に注意

次に説明するのはキャッシュからの情報漏洩です。こちらの方が、ありがちな問題だと思います。
キャッシュ制御が不十分な場合、プロキシサーバー等にキャッシュされた秘密情報が、別人のブラウザに表示される場合があります。
以下は、キャッシュ制御脆弱な例です。現在のログインIDを表示するページの実装例です。
<?php  // profile.php
  session_cache_limiter('public'); // キャッシュ制御をpublicに
  session_start();
  $id = $_SESSION['id'];  // ログインIDを取り出し
?><body>
id: <?php echo htmlspecialchars($id); // ログインIDを表示 ?>
</body> 
session_cache_limiter('public'); により、このHTTPメッセージはプロキシサーバーによりキャッシュされます。同じプロキシサーバー(実験ではUbuntu12.04上のsquid3を使用)に接続しているパソコン2台を用い、1台目のパソコンでwatanabeでログインし、2台目のパソコンで未ログインの状態で profile.phpを表示したところ、watanabeが表示されました。実験に使用したブラウザは2台ともIE11です。

キャッシュ制御不備による影響

キャッシュ制御不備により、同じプロキシサーバーの利用者が別人の個人情報を閲覧する(いわゆる別人問題)可能性があります。さらに、プロキシサーバーの仕様や設定によっては、Set-Cookieレスポンスヘッダがキャッシュ、共有されてしまい、セッションIDの共有によるセッションハイジャックが起こる可能性があります。
Set-Cookieヘッダのキャッシュに関しては、こちらこちらに言及があります。
プロキシー・サーバーが応答を受け取ったときにキャッシュに入れない set-cookie ヘッダーを指定します。デフォルトでは、プロキシー・サーバーは set-cookie ヘッダーをプロキシー・キャッシュに保管します。Cache-Control ヘッダー情報が正しく設定されていない場合、プロキシー・サーバーは、セッションに関連したユーザー・プライベート Cookie を保管する場合があります。
http://www-01.ibm.com/support/knowledgecenter/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/rjpx_siphttpcustprops.html?lang=ja より引用
上記は、IBM製品のドキュメントからの引用ですが、恐ろしいことが書いてありますね。これによるセッションIDの共有等が起きないように、適切にキャッシュ制御とプロキシサーバーの設定を行う必要があります。

対策

レスポンスヘッダによるキャッシュ無効化については、前述のとおりです。参照系ページの場合POSTメソッドは使えないので、キャッシュ制御をもれなく行うことで対策となります。また、リバースプロキシ(キャッシュサーバー)を使っている場合は、キャッシュのON/OFFが適切に実施されていることや、Set-Cookieヘッダのキャッシュが無効化されていることを確認してください。
キャッシュ制御の仕様を明確にするために、画面遷移図等にキャッシュ可否の印をつけるようにするとよいと思います。

まとめ

キャッシュ制御不備の脆弱性について紹介しました。後半に説明したキャッシュサーバーによる別人問題はわりあいよく知られた内容ですが、冒頭で説明した「ブラウザキャッシュによりログアウト処理が無効になる」という例は私自身初めて見たもので、珍しい例ではないかと思います。
どちらの例でも、キャッシュ制御を適切に行う、すなわち適切なレスポンスヘッダを送信することで対策になります。ただし、性能上の理由から、できるだけ多くのページをキャッシュさせたいというニーズもあるでしょうから、キャッシュの可否を仕様として明確にしておき、仕様通りのキャッシュ制御ができているかを確認するとよいでしょう。

フォロワー