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ヘッダのキャッシュが無効化されていることを確認してください。
キャッシュ制御の仕様を明確にするために、画面遷移図等にキャッシュ可否の印をつけるようにするとよいと思います。

まとめ

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

2015年3月17日火曜日

安全なウェブサイトの作り方改訂第7版の変更点と変わらない点

IPAの安全なウェブサイトの作り方 改訂第7版が公開されました。
このエントリでは、安全なウェブサイトの作り方の元々もつ特徴(変わらない点)と、第7版の変更のポイントについて説明します。
なお、私は安全なウェブサイトの作り方の執筆者の一人ではありますが、以下の記述は私個人の意見であり、IPAを代表するものではありませんので、あらかじめご承知おきください。

安全なウェブサイトの作り方の変わらぬ特徴

安全なウェブサイトの作り方の特徴は、「まえがき」の中で述べられています。
本書は、IPAが届出を受けたソフトウェア製品およびウェブアプリケーションの脆弱性関連情報に基づいて、特にウェブサイトやウェブアプリケーションについて、届出件数の多かった脆弱性や攻撃による影響度が大きい脆弱性を取り上げ、その根本的な解決策と、保険的な対策を示しています。
すなわち、以下の2点がポイントと考えます。
  • 脆弱性の選定基準として、届出件数の多かった脆弱性と攻撃による影響度が大きい脆弱性に注目している
  • 対処の方法として、根本的な解決策と保険的な対策という考え方を採用している
まず、取り上げる脆弱性の選定基準ですが、(1)現実に届け出の多いもの、すなわち現実のアプリケーションで発見される機会の多いものと、(2)必ずしも発見の機会は多くないが攻撃による影響度の大きな脆弱性という基準を採用しています。すなわち、脆弱性の選定は結構慎重な立場をとっていることになります。
さらに言えば、「これさえ対応しておけば大丈夫」ではなく、「最低限これらは対応してね」という感じ、と言えば分かりやすいでしょうか。

根本的解決策と保険的対策

次に、根本的解決策と保険的対策についてです。私見では、この着眼点こそ安全なウェブサイトの作り方の最大の特徴ではないかと思います。
まずは、両者の説明を安全なウェブサイトの作り方から引用します。
■ 根本的解決
本書における「根本的解決」は、「脆弱性を作り込まない実装」を実現する手法です。根本的解決を実施することにより、その脆弱性を狙った攻撃が無効化されることを期待できます。
■ 保険的対策
本書における「保険的対策」は、「攻撃による影響を軽減する対策」です。根本的解決とは違って、脆弱性の原因そのものを無くすものではありませんが、攻撃から被害までの次の各フェーズにおいて、それぞれの影響を軽減できます。
安全なウェブサイトの作り方では、「解決策」と「対策」を意図的に使い分けています。すなわち、脆弱性が混入する根本原因に注目し、その原因を取り除くことにより脆弱性のない状況を作ることを「解決策」と呼び、根本原因を取り除くことが困難であったり、抜け漏れが生じやすい場合の緩和策等を「(保険的)対策」と呼んでいます。

とくに、脆弱性が生まれる根本原因を示し、その根本的解決策を示したことは、安全なウェブサイトの作り方の大きな特徴だと考えます。海外のドキュメントを見ると、バリデーションなど必ずしも根本原因に根ざした対応になっていないものをよく見かけるだけに、なおのことそう思います。

第7版での変更点

次に第7版での変更点です。第7版の「まえがき」には以下のように書かれています。
第7版では、1章に、クリックジャッキングとバッファオーバーフローの脆弱性の解説を追加し、クロスサイト・スクリプティングの脆弱性への対策方法、各脆弱性で紹介している届出状況、参考URL等を更新しました。また、2章に、ウェブサイトにおけるパスワードの管理方法の解説を追加し、通信経路の暗号化の解説、DNSなどの対策方法、参考URLを更新しました。
ということで、久しぶりに第1章の脆弱性項目が下記2項目追加されて脆弱性項目が11項目になりました。これは、第3版で9項目になって以来の追加となります。
  • クリックジャッキング
  • バッファオーバーフロー
企業等で、安全なウェブサイトの作り方を元にセキュリティ基準等を定めているところがかなりあると見聞きしていますが、それら企業・団体は、この機会に基準の改定を推奨いたします。

クリックジャッキングについて

クリックジャッキングとは、iframe等を用いてウェブサイトの利用者を視覚的に騙し、意図しないマウス操作をさせる技法です(下図)。


IPAからは、クリックジャッキングに関してテクニカルウォッチ「『クリックジャッキング』に関するレポート」という文書を公表しています(2013年3月26日)。それから2年が経過して、安全なウェブサイトの作り方に取り入れられたということです。
説明自体はテクニカルウォッチの方が詳しいので、詳細はそちらを参照いただければと思います。

バッファオーバーフローについて

おなじみのバッファオーバーフローも、安全なウェブサイトの作り方に収録されました。ただし、ウェブアプリケーションの開発によく用いられるPHP、Ruby、Java等を用いている限り、アプリケーションレベルでバッファオーバーフロー脆弱性が混入する可能性はないので、C/C++等を用いてウェブアプリケーションを開発しているケースや、アプリケーションから呼び出していライブラリがC/C++等で開発されていて、そこにバッファオーバーフロー脆弱性があるようなケースを想定しています。そのため、安全なウェブサイトの作り方での説明も非常に簡略なものになっています。

ソルト付きハッシュ値によるパスワードの保存

パスワードリスト攻撃では、あるサイトから漏洩したパスワードが別のサイトへの攻撃に悪用されていると言われています。このため、サーバーへの侵入による被害が当該サイトにとどまらず、他のサイトにも波及してしまうことが問題です。
このため、ウェブサイトで保管するパスワードに関しては、平文ではなく、保護された形で保管すことが望ましい言えます。安全なウェブサイトの作り方第7版では、ソルト付きハッシュ値を用いて(さらにストレッチングを用いて)パスワードを保護された形で保管することを勧めています。
現実のウェブサイトには、パスワードを平文保管しているサイトがまだ多いと推測しますが、少なくとも今後新規開発するウェブサイトについては、保護された形でのパスワード保管を必須とすべきと考えます。
パスワードの安全な保管については、下記の記事も参考にしてください。

まとめ

安全なウエブサイトの作り方について、従来からの特徴と第7版での主な変更点について説明しました。
安全なウェブサイトの作り方は、ウェブアプリケーション開発の現場では、発注時のセキュリティ仕様として、あるいは開発会社の自社のセキュリティ基準として用いられている場合が多いようです。この場合は、以下に気をつけるとよいでしょう。
  • 最低限のセキュリティ基準としてとらえ、アプリケーションの性質によっては対応する項目を追加することが望ましい
  • 改版で内容が改定されるので、発注仕様等として用いる場合は、版数を明記する
このような使われ方を想定して、IPAのダウンロードページでは、安全なウェブサイトの作り方の旧版(第6版)もダウンロードできるようリンクを追加しています。
また、特にクリックジャッキングとバッファオーバーフロー、ソルト付パスワードによるパスワード保管については、順次自社基準や発注時仕様に盛り込んでいかれると良いと思います。

2015年3月11日水曜日

HASHコンサルティングのイー・ガーディアングループ参加に関するお知らせ

既にご案内の通り、イー・ガーディアン株式会社HASHコンサルティング株式会社の全株式を取得し、完全子会社化することで合意いたしましたのでご案内いたします。

平たくというと、何が変わるの?

(1) HASHコンサルティング株式会社の株主が変わります
旧株主: 徳丸浩(100%)  →  新株主: イー・ガーディアン株式会社(100%)

(2) 本社が移転します
旧本社: 東京都品川区(自宅兼オフィス)
新本社: 東京都港区麻布十番1-2-3 プラスアストルビル 5F
    ※イー・ガーディアン株式会社の本社が入居しているビルです

(3) 社員を増やします
旧: 徳丸が一人でなんでもやっていました
新: 一緒に仕事をしてくれる技術者を募集します

変わらないことは何?

(1) 会社は存続します
 HASHコンサルティングという会社はイー・ガーディアン株式会社の子会社として存続し、社名も変わりません。

(2) 徳丸は引き続き代表取締役です
 今後も徳丸はHASHコンサルティングの代表取締役社長をつとめます。

(3) 従来の業務は継続します
 当然ながら、従来の業務は変わりなく継続します。

どうしてイー・ガーディアンにジョインしたか?

2008年4月に独立して以来、ウェブアプリケーションのセキュリティ一筋に一人でやってまいりました。創業以来何度か、知り合いの社長さん達からは、「徳丸さんとこは、事務所を借りないの? 人を増やさないの?」と水を向けられたこともあり、その都度いったんはその気になりかけるものの、いやいや自分はその器ではないと断念をしておりました。

社員を増やすことについては、2つの課題がありました。
一つは、こちらにも書いたとおり、独立の目的の一つとして、子供と接する時間を増やしたいということがありましたので、子供と接する時間を増やすことと、人を増やし会社を大きくすることとは相容れないと思っていました。
また、社員を雇用するとなると、人材の流動性が増したとは言え、社員の人生に対して一定の責任が生ずると考えます。自分の年齢的なものもあり、その責任は私には重いものでした。

しかし、創業から約7年がたった現在では、子どもたちは中学・高校生になり、親にべったりというよりは、友達との関係の方が大切な年頃になりました。「手が離れた」ということではありませんが、子供と接する時間の長さは、以前ほどは重要ではなくなりました。

そんな折、前職の同僚であるイー・ガーディアンの高谷社長からジョインのお声がけをいただき、これは会社を大きくするよい機会だと考えました。イー・ガーディアンの経営資源を活用させていただきながら、私の力量でも無理なく会社を大きくできるチャンスだと捉えました。

一人でできることは限られていますが、今後は理想と方法論を共有する仲間と力を合わせることにより、日本のインターネットを少しでも安全にできるよう尽力していきたいと考えます。

人材募集中

前述の通り、HASHコンサルティング株式会社では従来社員の募集をしておりませんでしたが、今後は積極的に人材を募集してまいります。まだ具体的な条件等は決まっていませんが、現在考えていることとしては下記のようなイメージとなります。
  • 学歴、性別、年齢、国籍等は不問です
  • ただし、徳丸は日本語しかできませんので、日本語でのコミュニケーションがとれることは必要です(少なくとも当面は)
  • 一人からの立ち上げとなりますので、当面は、ある程度セキュリティ実務(脆弱性診断等)の経験のある方を歓迎いたします
  • 勤務地は本社(麻布十番)となりますが、今後サテライト勤務や在宅勤務等についても検討していきたいと考えます
  • 服装、勤務時間等は、集まってくれた仲間と相談しながら、働きやすい職場にしていきます
  • HASHコンサルティングのサービスの概要はWebサイトを参考にして下さい
興味を持たれた方は、facebooktwitterのメッセージまたは問い合わせページからご連絡ください。メールでも構いません。

ということで今後は、イー・ガーディアングループの一員としてのHASHコンサルティング株式会社をよろしくお願い申し上げます。
ありがとうございました。

2015年3月2日月曜日

Webアプリケーション脆弱性診断の検査対象をどう絞り込めばよいか

ソニーDNAさんの『入門!基礎からわかる「失敗しないWeb診断業者の選び方」』というブログ記事を読みました。
全体的に穏当な内容で異論はないのですが、興味深い内容なので、屋上屋を架すようですが少し追加して考えてみたいと思います。
私が特に注目したのは以下の箇所です。
2. 検査対象を適切に絞れるか?
セキュリティ対策をくまなく実施できれば安心ですが、それは大きな費用がかかり現実的ではないというケースも多いでしょう。そのため、Web診断では検査対象を適切に絞り込むことが必要です。ログイン画面や課金機能、個人情報管理機能など、セキュリティ対策が特に求められる機能を重点的に検査するには、検査対象を明確にすることが重要になります。
上記の考え方は、脆弱性診断の現場でよく行われているもので、筆者もこれに従うことは多いのですが、検査対象の選定は重要なのでもう少し掘り下げて考えてみたいと思います。

脆弱性と影響範囲の関係

検査対象を検討する上での前提条件として、脆弱性がある箇所(Webページ等)が及ぼす影響範囲がどの程度であるかを考える必要があります。
以前の記事ですが、「SQLインジェクション対策もれの責任を開発会社に問う判決」を書いた際に、私は以下のように書きました。

  • Y社は、SQLインジェクションはカード情報とは無関係の箇所にあったので、この脆弱性が原因ではないと主張したが、裁判所はこの主張を退けた

開発会社側は、カード情報と無関係の箇所にSQLインジェクションがあってもカード情報漏洩の原因にならないと主張していましたが、裁判所の判断通り、この主張は間違いです。サイト上のどこにSQLインジェクション脆弱性があっても、その影響はサイト全体に及びます。
このように、脆弱性の中には、影響範囲が広いものと、脆弱性のあるページ等に影響が限定されるものがあります。

以下は、影響範囲が広範囲に及ぶ脆弱性の例です。
  • SQLインジェクション
  • クロスサイトスクリプティング(XSS)
  • ディレクトリトラバーサル
  • OSコマンドインジェクション
  • HTTPヘッダインジェクション
  • セッション固定
  • ファイルアップロード機能の脆弱性
  • クッキーのセキュア属性不備
  • 認証機能の不備
一方、以下は影響範囲が脆弱のある箇所に限定されるものの例です。
  • クロスサイトリクエストフォージェリ(CSRF)
  • レースコンディション(サイト全体に影響がある場合もある)
  • 認可制御の不備
以下は、サイトには直接影響のない脆弱性の例です。
  • オープンリダイレクタ(フィッシング経由で間接的に影響はある)
  • メールヘッダインジェクション
このように見ていくと、「やっぱりサイト全体を診断しないと駄目なのでは?」という疑問が生じます。もちろん、それが望ましいことは言うまでもなく、元記事でもそのように言及していますが、診断に掛けられる予算が限定されている場合に、ある根拠をもって診断箇所を絞り込むことは可能です。その理由を以下に述べます。

脆弱性毎に、発生しやすい箇所が決まっている

ここまで、脆弱性の影響範囲を検討したので、今度は脆弱性が入り込みやすい場所について検討していくことにしましょう。

SQLインジェクション
SQLインジェクション脆弱性は、SQLアクセスをしている箇所すべてで発生する可能性があります。加えて、その影響がデータベース全体に及ぶことから、可能であればすべての箇所を検査しておくことが望ましいことになります。

クロスサイトスクリプティング(XSS)
クロスサイトスクリプティング脆弱性は、表示処理(HTML等の生成)を行っている箇所すべてで発生する可能性があります。加えて、その影響がWebサイト全体に及ぶことから、可能であればすべての箇所を検査しておくことが望ましいことになります。

ディレクトリトラバーサル
ディレクトリトラバーサル脆弱性は、外部から指定したファイル名によりファイルアクセスしている箇所で発生する可能性があります。GETやPOST等のパラメータがファイル名であるかどうかは外部からは完全には分かりませんが、パラメータ名や値から、ファイル名の可能性が高いと推測できる場合もあります。

OSコマンドインジェクション
OSコマンドインジェクション脆弱性は、外部コマンドを呼び出している箇所で発生する可能性があり、その典型例がメール送信処理の箇所です。また、Web開発に用いる言語が提供していない機能を実現する場合に外部コマンドを呼び出していて、OSコマンドインジェクション脆弱性が発生する場合もあります。

HTTPヘッダインジェクション
HTTPヘッダインジェクション脆弱性は、レスポンスヘッダの出力時に外部由来の文字列が混入しているケースで発生する可能性があり、その典型例がログイン後のリダイレクト処理やクッキー発行処理です。サイトを閲覧すればリダイレクト処理やクッキー発行をどこで行っているかは分かるため、そこだけを診断すればよいことになります。

セッション固定
セッション固定の原因はログイン成功時にセッション固定を防ぐ対策(典型的にはセッションIDの変更)がないことであり、ログイン処理を調べることで診断が可能です。

ファイルアップロード機能の脆弱性
ファイルあっブロード機能の脆弱性としては、スクリプトをアップロードしてサーバー側で実行する、あるいは利用者にJavaScriptを実行させるXSS等があります。いずれもアップロード処理と、それに関連するダウンロード処理を調べることで診断ができます。

クッキーのセキュア属性不備
クッキーのセキュア属性不備は、重要な情報をもつクッキー(セッションIDやトークン等)にセキュア属性がついていることを確認することで診断します。サイト全体を巡回して診断すべきですが、典型的なケースについてはログイン時のクッキー発行で診断することが可能です。

認証機能の不備
認証の不備は主にパスワードの扱いに関するもの、アカウントロック等のセキュリティ機能、ログアウト処理(これはセッション管理の一部として診断する場合もある)などの診断です。最近見かける頻度は少なくなりましたが、こちらで紹介したようなぶっ飛んだものもたまにあります。
診断する箇所は主にログイン機能と、ログアウト機能です。

脆弱性には遍在するものと偏在するものがある

どちらも「ヘンザイ」でややこしい見出しですが、XSSのようにどこにでもある(遍在)脆弱性と、HTTPヘッダインジェクションのように特定の箇所にしかない(偏在)脆弱性があるという意味です。
そして、ログイン処理のようなセキュリティ上重要な処理には、HTTPヘッダインジェクションのような偏在性のある脆弱性がわりあい存在しやすいという性質があります。そして、「認証の不備」のような脆弱性は、当然ながらログイン処理の周辺を診断することになります。
すなわち、抜き取り診断において、ログイン処理などセキュリティ上重要な箇所に絞って診断することの *一応の* 根拠は、以下の通りです。
  • ログイン処理など当該機能の脆弱性を診断するのに必要
  • 一部の脆弱性はログイン処理の周辺に発生しやすいものがある
しかし、これはあくまで傾向的なものであり、絶対的なものとまでは言えません。

抜き取り検査の診断箇所はどのように選定するのがよいか

脆弱性診断の現場の運用では、抜き取り検査として「ログイン処理などセキュリティ上重要な箇所」に絞って診断することはよく行われますが、それだけだと検査漏れが生じやすくなる可能性もあります。
たとえば、エラー表示のページのように、機能的にもセキュリティ上もあまり重要とは言えないページに、SQLインジェクションやXSSのような脆弱性があった場合、その影響はサイト全体に及び、個人情報など重要な情報が漏洩する原因になりえます。
この可能性をゼロにするには、サイト全体を網羅的に診断するしかありません。しかし、予算の関係で網羅的な診断ができない場合は、重要箇所に加えて、経験上「脆弱性の出やすいところ」を診断対象に加えるとよいでしょう。先に述べたエラー表示のページは、セキュリティ上重要でないがゆえに、一種の油断が生じ、脆弱性が入りやすい傾向があるように思います。
安全なウェブサイトの作り方別冊として公開されている「ウェブ健康診断仕様」には、診断そのものの手法に加えて、抜き取り箇所についても一応の基準が説明されています。

まとめ

脆弱性診断の検査箇所を検討する際の考え方について紹介しました。現場でよく行われる「セキュリティ上重要な箇所のみを診断する」ことについて、一応の根拠はあるものの、それだけで重要な情報が守られるわけではありません。このため、抜き取り検査の検査箇所を工夫することにより、同じコストであってもより精度の高い診断が期待できます。
また、「Web診断業者の選び方」として、診断箇所を絞り込む際のリスクや、現実的な対応等について、適切な助言を与えてくれる業者を選ぶとよいでしょう。

2015年2月23日月曜日

PHPのmb_ereg関数群は不正な文字エンコーディングをチェックしない

PHPのbasename関数には、マルチバイトに対応していないという誤解(実際にはロケールの設定をすればマルチバイトでも使える)があったり、不正な文字エンコーディングをチェックしないという課題があったりで、イマイチだなーと思っている方も多いと思います。
そういう方々が、preg_replace(u修飾子つき)やmb_ereg_replaceを用いて代替関数を作成している解説も見かけますが、それではこれら正規表現関数は不正な文字エンコーディングをチェックしているのだろうかという疑問が生じます。
ざっと調べたところ、以下の様な状況のようです。
  • preg_replace : 不正な文字エンコーディングをチェックしている
  • mb_ereg_replcae : 不正な文字エンコーディングをチェックしていない
ここでは、mb_ereg_replaceが不正な文字エンコーディングをチェックしない状況と、その影響について報告します。

不正な文字エンコーディングとは

マルチバイトの文字を表現するエンコーディング(Shift_JIS、EUC-JP、UTF-8など)には、バイト列の並びのルールが決まっています。例えば、Shift_JISの2バイト目としてありえるバイト値とあり得ないバイト値があります。UTF-8の場合は、先頭バイトで文字のバイト数が決まり、2バイト目以降に使えるバイトは 0x80 ~ 0xBF と決まっています(参考)。
これらの決まりに従わないバイト列は、「不正な文字エンコーディング」ということになります。また、UTF-8には「非最短形式」というものがあり、禁止されているので、これも不正な文字エンコーディングの一種です。

mb_ereg_replaceは不正な文字エンコーディングをどう扱うか?

ここでは、UTF-8の場合を例として、mb_ereg_replaceが不正な文字エンコーディングのデータをどのように処理するかを見てみます。
まず、不正なデータとして以下を例に取ります。
$a = "\xFC../..";  // 6バイト
このデータは以下の意味でUTF-8として不正です。
  • 0xFCは、かつてUTF-8の6バイト形式として定義されていたが、現在は5バイト以上の形式は禁止されている
  • 2バイト目以降が 0x80~0xBF の範囲にない
PoCは以下の通りです。
<?php
  mb_regex_encoding('UTF-8');
  $a = "\xFC../..";
  $s = mb_ereg_replace('[\./]', '', $a);  // . と / をすべて取り除く
  echo bin2hex($s) . "\n";
結果は以下の通り。
fc2e2e2f2e2e
先頭の0xFCの影響を受けて、2バイト目以降の . や / は取り除かれていません。ここから以下のことが分かります。
  • mb_ereg_replaceはUTF-8の6バイト形式を許容している
  • mb_ereg_replaceはUTF-8の2バイト目以降のバイト値の範囲をチェックしていない
この結果は私には衝撃的だったのですが、ディレクトリトラバーサル脆弱となる現実的なシナリオは作れませんでした。腕自慢の方は挑戦してみてください。

脆弱となる例

ディレクトリトラバーサルの例は作れませんでしたので、XSSならどうだろうと思い、ちょっと人工的ですが、不正な文字エンコーディングによるXSS脆弱性の例を考えてみました。以下のスクリプトはmb_ereg_replaceを使った「手作りの」HTMLエスケープ関数を使っています。
<?php
  header('Content-Type: text/html; charset=UTF-8');
  function mb_htmlescape($s) {
    mb_regex_encoding('UTF-8');
    $s = mb_ereg_replace('&', '&amp;', $s);
    $s = mb_ereg_replace('<', '&lt;', $s);
    $s = mb_ereg_replace('>', '&gt;', $s);
    $s = mb_ereg_replace('"', '&quot;', $s);
    return $s;
  }
?>
<html><body>
<?php echo mb_htmlescape($_GET[x]); ?>
</body></html>
このスクリプトに対して、x=<script>alert(1);</script> というクエリ文字列を与えると以下のように正しくエスケープ処理が行われています。


しかし、以下のクエリ文字列だと、JavaScriptが起動されてしまいます。
x=%C2<script+%C2>alert(1);//%C2</script+%C2>

%C2はUTF-8の2バイト形式の1バイト目になるバイト値です。これを■で表現すると、入力文字列は下記の通りです。
■<script ■>alert(1);//■</script ■>
そして、この文字列は、先の手作りエスケープ関数を素通りしてそのままブラウザに送られます。
ブラウザ側では、この■<等が「不正なUTF-8文字」と認識して、■と < という別々の文字として扱われます。この「不正な文字に対する扱いの差異」が脆弱性の原因です。

対策

以下の対策を推奨します。
  • エスケープ関数等セキュリティ目的の処理は極力自作しない(htmlspecialchars関数では上記の問題は起きない)
  • 各入力値についてmb_check_encoding関数により文字エンコーディングの妥当性チェックを行う

まとめ

PHPのhtmlspecialcharsはかつていけてなかった(参考)とか、basename関数は今もいけてないなどの理由でこれらの関数を自作したくなる衝動にかられる場合がありますが、中途半端に自作するとかえって危険になる場合があります。PHPの主要な関数群は色々ディスられながら改良され、安全になってきた歴史がありますので、よほど明確な理由がない限りはPHPで提供されているものを使うほうがよいと考えます。
また、PHPが提供する関数群には文字エンコーディングのチェックを厳密に行うもの(mb_check_encoding、htmlspecialchars等)と、チェックをあまりしないもの(mb_strlen、basename、mb_ereg等)がありますので、入力値のバリデーション時にmb_check_encodingで文字エンコーディングの妥当性を確認しておくとよいでしょう。

2015年2月13日金曜日

PHPのbasename関数は不正な文字エンコーディングをチェックしない

昨日のエントリにて、PHPのbasename関数はマルチバイト文字を扱えることを説明しましたが、このブログの読者であれば、きっとbasename関数は不正な文字エンコーディングについてどの程度チェックするのかという疑問が生じたことでしょう(きっぱり)。実はbasename自体は、不正な文字エンコーディングをチェックせず、垂れ流してしまいます。その理由をbasenameのソースコードで確認してみましょう。以下は、basename関数の実装の一部です。
// ext/standard/string.c
// php_basenmae()
while (cnt > 0) {
  inc_len = (*c == '\0' ? 1: php_mblen(c, cnt));

  switch (inc_len) {
    case -2:
    case -1:
      inc_len = 1;
      php_ignore_value(php_mblen(NULL, 0));
      break;
    case 0:
      goto quit_loop;
php_mblen関数はmblen(3)のラッパーです。mblen関数は文字列の先頭文字のバイト数を返す関数で、先頭の文字が不正なエンコーディングの場合 -1 を返します。上記のソースでは、mblenが-1を返した場合は、inc_len=1として正常な1バイト文字と見なして処理を継続しています。
一方以下は、シェル呼び出しのエスケープを行うescapeshellarg関数の実装ですが…
// ext/standard/exec.c
// php_escape_shell_arg()
for (x = 0; x < l; x++) {
  int mb_len = php_mblen(str + x, (l - x));

  /* skip non-valid multibyte characters */
  if (mb_len < 0) {
    continue;
  } else if (mb_len > 1) {
    memcpy(cmd + y, str + x, mb_len);
    y += mb_len;
    x += mb_len - 1;
    continue;
  }
  switch (str[x]) {
    // 文字に応じた処理
コメントにあるように、不正な多バイト文字はスキップする、すなわち除去(フィルタリング)されます。

同じmblen関数を使っていても、basename関数とescapeshellarg関数では、不正な文字エンコーディングに対する対処方法が違っています。ともかく、basename関数は、不正な文字エンコーディングをエラーとせず、結果の中に含めてしまいます。

不正な文字エンコーディングの影響の考察(Windowsの場合)

basename関数が不正な文字エンコーディングのチェックをしていないことによるセキュリティ上の影響はないでしょうか。具体的に確認するために、まずWindowsの場合について検討します。すなわち、ファイル名が Shift_JIS でエンコーディングされているとします。
ディレクトリトラバーサル攻撃の攻撃パターンとしては、絶対パスによるものと相対パスによるものがありますが、絶対パスの場合ファイル名の冒頭に / や \ が来る必要があり、これは「不正な文字エンコーディング」にはなりえません。相対パスの方は、 ../ などシングルバイトの文字が連続して続く必要がありますが、/や\が単独の場合は単に除去され、その前にマルチバイト文字の先行バイトがある場合でも、前述の理由から先行バイトは除去されません(..■/ の形になる)。.その後 / までが除去される可能性が高いですが、仮に除去されない状況でも、..■/ と余計な文字がはさまっているため、攻撃パターンを形成しないと思われます。

不正な文字エンコーディングの影響の考察(Linuxの場合)

次にLinuxの場合について考えます。文字エンコーディングは UTF-8 とします。この場合、basename関数はUTF-8の冗長表現を通してしまいます。
これを検証するためのスクリプトを以下に示します。\xC0\xAF は / をUTF-8の2バイト表現にしたものです。
<?php
  setlocale(LC_CTYPE, 'ja_JP.UTF-8');
  echo bin2hex(basename("..\xC0\xAFaaa")), PHP_EOL;
出力は下記となります。c0afがそのまま出力されていることがわかります。
2e2ec0af616161
UTF-8の冗長表現が許可されるというと、NimdaワームやTomcatの脆弱性CVE-2008-2938を思い出す人も多いと思います。「それは問題ではないか」と思うところですが、現実には問題になるケースはほとんどないと考えられます。

その理由は下記のとおりです。
  • Linuxで使われるファイルシステムでは、\xC0\xAF等はそのままのバイト列としてファイル名に使われ、ディレクトリ区切子とは認識されない
そこで次の可能性は、UTF-8の冗長表現表現としてbasenameをパスした文字列が、その後文字エンコーディング変換されてシングルバイトの / に変換されることですが、
  • そもそもbasenameの後に文字エンコーディング変換をすることはよろしくない(参考
  • PHPで文字エンコーディング変換に使用される mb_convert_encodingとiconvはどちらもUTF-8の冗長表現をエラーにするかフィルタリングするので、攻撃文字列は形成されない
ということで、basename関数が冗長なUTF-8エンコーディングを許容しても、実害が出るケースはほとんどないと考えられます。実害があるとすると、独自実装の脆弱性のある文字エンコーディング変換機能を利用している場合ですが、その場合でも文字エンコーディング変換後にbasename関数を通すという正しい手順を踏んでいれば、問題は顕在化しません。

緩和策

basename関数は不正な文字エンコーディングを許容することが分かりましたが、これによる実害はほとんどなさそうです。ただし、外部から与えられたファイル名で新規にファイルを作成する場合は、変なファイル名のファイルができてしまいます。
いずれにせよ、以下をアプリケーションの仕様として決めておくとよいでしょう(再掲)。
  • ファイル名に用いる文字の種類
  • ファイル名を表現する文字エンコーディング
  • ファイル名の長さの最小値・最大値
そして、以下を推奨します。
  • 文字エンコーディングの変換はbasenameを通す前に行うこと
  • basename関数を呼ぶ前にlocaleを設定すること
  • ファイル名の仕様を決める
  • ファイル名が(文字エンコーディングを含め)仕様を満たすかどうかバリデーションにより確認する

まとめ

PHPのbasename関数が不正な文字エンコーディングを許容してしまうことを説明しました。この問題は一応bug#68773として報告済みですが、報告から1ヶ月以上たってもアサインもされていませんので、少なくともすぐに修正される可能性は薄そうです。幸い実害もあまりなさそうですが、念のためバリデーションにより文字エンコーディングのチェックをしておくと安心です。
PHPの文字列は単なるバイト列ですので、一般論として、アプリケーションの開始時に文字エンコーディングのチェックをしておくことにより、不正な文字エンコーディングの文字を弾いておくことをお勧めします。アプリケーションの前提条件を満たしていない入力を予め除外しておくことでアプリケーションの安定動作のために寄与します。
また、basenameの現在の実装は少々いただけないと考えます。せっかくmblenが不正な文字エンコーディングをチェックして -1 を返しているのに、そのエラーを「なかったことに」しているからです。一方、シェルのエスケープを行うescapeshellargの方は不正な文字エンコーディングをフィルタリングしているわけで、同じPHPの中で一貫性のない挙動というのも(PHPらしいといえばそれまでですが)よくないように感じました。

2015年2月12日木曜日

PHPのbasename関数でマルチバイトのファイル名を用いる場合の注意

まずは以下のサンプルをご覧ください。サーバーはWindowsで、内部・外部の文字エンコーディングはUTF-8です。UTF-8のファイル名を外部から受け取り、Windowsなのでファイル名をShift_JISに変換してファイルを読み込んでいます。basename関数を通すことにより、ディレクトリトラバーサル対策を施しています。
<?php
  header('Content-Type: text/plain; charset=UTF-8');

  $file_utf8 = basename($_GET['file']);
  $file_sjis = mb_convert_encoding($file_utf8, 'cp932', 'UTF-8');
  $path = './data/' . $file_sjis;
  var_dump($path);
  readfile($path);

しかし、ディレクトリトラバーサル対策は十分でなく、このスクリプトには脆弱性があります。下図は、ディレクトリトラバーサル攻撃により、このスクリプトの中身を読み出しているところです。


以下、このスクリプトの問題点、さらにはbasename関数を用いる際の注意点について説明します。

basename関数はマルチバイト対応していないという誤解

ネットの記事を見ていますと、basename関数はマルチバイト対応していないという主張をよく見かけます。例えば、下記の記事。
basename関数はパスからディレクトリ情報を削除してベース名(「test.txt」など)を取得するための関数ですが、PHP5(使用バージョン:PHP 5.3.1)ではパスに日本語が含まれていると失敗します。

▼失敗するbasename関数
<?php
$a = "/dir/テスト.txt";
echo basename($a);
?>
ファイル名などに日本語が含まれるパスでbasename関数が失敗するバグより引用
このスクリプトをWindowsサーバー上で動かすと、確かに下図のようにファイル名が化けます。下図ではファイル名の16進数表記を参考のためつけています。「テ」の先頭1バイト0x83が欠落しています。



localeに注意

しかし、このスクリプトはbasename関数の使い方に問題があります。basenameのマニュアルには以下の注意書きがあります。
注意:
basename() はロケールに依存します。 マルチバイト文字を含むパスで正しい結果を得るには、それと一致するロケールを setlocale() で設定しておかなければなりません。
これに従い、先のスクリプトを修正してみます。
<?php
$a = "/dir/テスト.txt";
setlocale(LC_CTYPE, 'Japanese_Japan.932'); // 追加
echo basename($a);
こうすると、下図のように正しい結果が得られます。マルチバイト環境でbasename関数を利用するにはlocaleの設定が必要であることが分かります。


冒頭の脆弱なスクリプトへの攻撃法

ここで、冒頭の脆弱なサンプルがなぜ問題かを説明します。このスクリプトにもlocaleの指定がありませんが、それが根本原因ではありません。
まず、攻撃に用いる文字列を示します。以下のクエリ文字列により攻撃が可能です。
..%C2%A5vulbasename.php
%C2%A5はUnicodeの円記号(U+00A5)のUTF-8表記をパーセントエンコードしたものです。これをデコードすると以下の通りです。
..¥vulbasename.php
「¥」は前述のとおり円記号U+00A5です。さらに、これをcp932に変換することになりますが、この際に「¥」U+00A5がバックスラッシュ「\」0x5Cに変換されます。
..\vulbasename.php
このため、組み立てられるパス名は以下の通りとなります。これは典型的なディレクトリトラバーサル攻撃の文字列ですね。
./data/..\vulbasename.php

文字エンコーディング変換するタイミングに注意

basename関数を通しているにもかかわらず攻撃が成立してしまう理由は、以下の通りです。
  • 円記号U+00A5は、basenameの処理対象の文字ではない
  • basenameの処理の後に、文字エンコーディング変換によりU+00A5が0x5Cに変化する
正しい手順は下記のとおりです。
  • まず文字エンコーディングを変換する
  • その後にbasename関数を通す
スクリプトとしては下記のとおりです。
$file_utf8 = $_GET['file'];
$file_sjis = mb_convert_encoding($file_utf8, 'cp932', 'UTF-8'); // 文字エンコーディング変換
setlocale(LC_CTYPE, 'Japanese_Japan.932'); // locale設定
$file = basename($file_sjis);
$path = './data/' . $file;
このスクリプトに対して先の攻撃をかけても、以下のようにスクリプトの中身は表示されません。


basenameはファイル名として妥当な文字種のチェックをしない

basename関数のソースを読むとすぐ分かりますが、basename関数がチェックするのはスラッシュ(全プラットフォーム共通)、バックスラッシュとコロン(Windows等)のみです。特にWindowsの場合ファイル名に使える文字に制約がありますが、basenameはそのチェックはしません。長さのチェックもしません。
このため、ファイル名を外部から受け取る場合、ファイル名の仕様として以下を決めておくとよいでしょう。
  • ファイル名に用いる文字の種類
  • ファイル名を表現する文字エンコーディング
  • ファイル名の長さの最小値・最大値
特に、外部から受け取ったファイル名でファイルを新規作成する場合は、受け取ったファイル名が仕様を満たすことをバリデーションで確認する必要があります。既存のファイルをオープンするだけであれば、バリデーションが必須かどうかは悩ましいところですが、一般論として入力値が前提条件(仕様)を満たすことのチェックとしてバリデーションはしておくべきだと思います。

まとめ

PHPのbasename関数を用いる上での注意点を説明しました。まとめると以下のようになります。
  • 文字エンコーディングの変換はbasenameを通す前に行うこと
  • basename関数を呼ぶ前にlocaleを設定すること
  • ファイル名の仕様を決める
  • ファイル名が仕様を満たすかどうかバリデーションにより確認する

フォロワー