なぜPHPアプリにセキュリティホールが多いのか?

第25回PHPのアキレス腱 ─⁠─ セッション管理

PHPにはHTTPセッション管理モジュールが標準で付いてきます。このセッションモジュールには非常に重大なセキュリティ上の脆弱性が修正されずに残っています。その脆弱性とはセッションアダプションです。

セッションアダプションとは、セッション固定化攻撃に利用される脆弱性です。PHPのセッション管理モジュールがセッションアダプションに脆弱であることは、かなり以前、何年も前から知られています。しかし、開発者の理解不足より脆弱性が放置されたままになっています。

セッションアダプションとは

セッションアダプションとは、ブラウザ等から送信された未初期化セッションIDをそのまま利用してセッションを初期化してしまう脆弱性です。ユーザが送信してきたIDでも第三者に予想できない文字列であれば大丈夫なのでは?と考える方もいると思います。その通りで第三者に予想できなければ問題ないですし、仮に予想できてもログインする際に別のセッションIDに変更できれば問題にはなりません。

セッションアダプションを理解するためにもっとも分かりやすい例は、URLやフォームに埋め込んだセッションIDを有効にしている場合の攻撃手順です。

URLベースのセッションアダプション攻撃

攻撃者はURLにセッションIDを埋め込んだメールやページを作成して、被害者にクリックさせます。セッションIDはURLに埋め込んであるので、セッションIDがランダムな文字列であっても攻撃にはとっては既知のセッションIDとなります。

  • http://target.example.com/?PHPSESSID=1234567890

セッションアダプションに脆弱なセッション管理機構は、URLに記載されたセッションIDによって初期化します[1]⁠。このため、攻撃者は有効なセッションIDを知るためにランダムなセッションIDを試さなくても、攻撃者が発行したセッションIDの中から有効なセッションIDを探すことができます。

(攻撃用の)セッションIDはメールごとやリクエストごとに別々のIDを使ってもよいですし、WebページならクリックのたびにJavaScriptを利用してユニークなセッションIDを与えても構いません。

Webページであれば、XMLHTTPRequestを利用してJavaScriptに一手間かければ、同じユーザには常に同じセッションIDで初期化するようにもできます。どのセッションIDが有効であるのか、データベースで管理することも可能です。

このように有効なセッションIDが分かれば、攻撃者は他人に成りすましてWebアカウントを利用できるようになります。

セッションアダプション対策

一般的にはセッションアダプション対策には、ログイン時にセッションIDを再生成すればよい、と解説されています。ログイン処理の前に、セッションIDを作り直す関数を呼び出すだけです。

例:セッションIDの再生成
session_regenerate_id();

PHPの場合、URLベースのだけのセッション管理をしていれば、PHPのセッション管理モジュールを使っていても、セッションIDの再生成によりセッションアダプション攻撃はできなくなります。

注意:URLベースのセッション管理はセッションID漏洩の問題をどうしても回避できないので、通常は使うべきではありません。URLベースのセッションIDのほうが安全ということではありません。

筆者が執筆したWebアプリセキュリティ対策入門でも、ログイン時にセッションIDを再生成するよう記載しています。実はこの対策だけでは不十分であることは、筆者がWebアプリセキュリティ対策入門を執筆した時点(2005年末)でも知っていました。筆者がこの本を書いている時点では、近い将来リリースされるPHPでセッションアダプション脆弱性が修正されると考えていたため解説を省略していました。

しかし、実際には現在に至るまでPHPのセッションモジュールのセッションアダプション脆弱性は修正されないままになっています。このために、本来はsession_regenerate_id関数をログイン処理を行う前に呼び出すだけでよいはずのログイン処理に、脆弱性に対応するための余計な手間が必要となっています。

脆弱性への対応法は最後に紹介します。まずはより現実的な脅威であるクッキーベースのセッション管理がなぜ(セッションアダプションに関して)危険であるのか解説します。筆者は個別のブラウザとアプリケーションやその構成に対して、どの攻撃方法が有効であるか知っています。しかし、この連載は攻撃方法を指南するための連載ではないので、個別かつ具体的な攻撃方法は解説しません。筆者が解説しないから、安全であるかというとそうではありません。

この連載を読まれているWeb開発者くらいの、通常のWebアプリケーション開発者のスキルがあれば、ここに書いてある攻撃方法は理解できます。開発者以外の方がこの記事を読まれていることは少ないと思いますが、条件が整っていれば攻撃は非常に簡単に行えると理解してください。

セッションアダプションとクッキー

先ほど「URLベースのセッション管理であれば、session_regenerate_id関数でセッションIDを再生成すれば、セッションアダプションの影響を受けない」と解説しました。実際に、URLベースのセッション管理⁠だけ⁠を利用し、適切なログイン処理を行っていれば、これから解説する脆弱性の影響を受けません。しかし、現在のPHPのデフォルト設定では、別のセキュリティ上の理由から、URLベースのセッション管理はデフォルトでは無効に設定されています。

通常のセッション管理はクッキーを利用して行われます。ブラウザのクッキーの取り扱いの仕様により、セッションアダプションに脆弱なセッション管理機構ではsession_regenerate_id関数を呼び出してもセッションアダプション脆弱性を回避できない状態が複数存在します。

クッキーの仕様

セッションアダプションがクッキーベースのセッション管理の場合にセキュリティ上の問題となる原因は、クッキーの仕様にあります。クッキーの仕様により、同じURLであっても複数のクッキー値を持つことができます。

例えば、

  • http://blog.ohgaki.net/2009/

であれば、最大4つのクッキー値 ─⁠─ 同じ変数名(クッキー名)で4つの値 ─⁠─ を持つことができます。

  • blog.ohgaki.net ドメインに対して⁠/⁠パスと⁠/2009/⁠パスの2つのクッキー
  • ohgaki.net ドメインに対して⁠/⁠パスと⁠/2009/⁠パスの2つのクッキー

これは、PHPのsessionモジュールのデフォルトのセッション名であるPHPSESSIDに4つの異なるクッキーを与えられることを意味します。

すべてのブラウザでどのクッキーが優先されるのか決まっており、一つのクッキーだけが送信されればよいのですが、現実は違います。ブラウザは同じクッキー名が複数ドメインや複数パスで設定されている場合、すべてのクッキーをWebサーバに送信します。ブラウザはクッキー名と値のペアを設定されている数だけ送信します。分かりやすくいうと、どのクッキーがどのドメインのどのパス用に設定されたのか、サーバ側では分からない形で、ブラウザの好き勝手な順序で送信します。

この仕様がセッションアダプションに脆弱なセッション管理機構に致命的なセキュリティ問題の原因となっています。

PHPのセッションモジュールの仕様

PHPのセッションモジュールは同じセッションID名(デフォルトはPHPSESSID)を持つクッキーが複数送信されてきた場合、最初のクッキーを優先して利用します。つまり、後に続くクッキーは無視されます。

PHP以外のセッション管理機構では、最後に設定されたクッキーが優先されるものもあります。

ブラウザの仕様

さらに話を複雑にするのはブラウザの仕様です。Webアプリケーションセキュリティの要といえるクッキーですが、実は明確な仕様が決まっていません。にわかには信じ難いと思う方もいるかも知れませんが、本当に明確な仕様が無いのです。

現在ブラウザに実装されているクッキーは、Netscape社のNetscapeブラウザのクッキー仕様と実装をベースにしています。Netscape社はクッキーの仕様を公開していましたが、この仕様は非常にいい加減で、複数のクッキーが設定されている場合、どのように取り扱うか決まっていません。

その結果、ブラウザによって複数のクッキーが設定されている場合にどのような順序で送信するのかばらばらになっています。

クッキーを設定する場合、ドメインとパス属性を使ってクッキーを設定できますが、どのクッキーがどのような条件で設定されたクッキーであるのかもまったく分からないようになっています。

クッキーベースのセッション管理の攻撃

クッキーベースのセッション管理機構のセッションアダプションの攻撃方法には、ドメインを利用する方法、パスを利用する方法、クッキーファイルを利用する方法の3種類があります。

ドメインを利用する方法

PHPのセッションモジュールは、デフォルトではURLのホスト名部分のすべての文字列を利用してセッションID用クッキーを設定します。

ブラウザによっては親ドメインに設定されたクッキーを最初に送信します。親ドメインにセッションID用クッキーが設定されていると、PHPのセッションモジュールは完全ホスト名ではなく、親ドメインのクッキーを優先します。

例えば、筆者の個人ドメインであるohgaki.netでは

  • www.ohgaki.net
  • wiki.ohgaki.net
  • blog.ohgaki.net

の3つのDNS名で運用しています。もし、www.ohgaki.netのアプリケーションにJavaScriptインジェクション脆弱性があった場合、JavaScriptを利用してohgaki.netドメインのPHPのデフォルトセッション名であるPHPSESSIDクッキーに特定の値を設定可能です。

筆者が親ドメインのクッキーを先に送信してしまうブラウザを使いながら、www.ohgaki.netのJavaScriptインジェクション脆弱性によって攻撃されると、セッションの固定化攻撃に成功する可能性があります。wiki.ohgaki.net以外のwww.ohgaki.netとblog.ohgaki.netで利用しているブログアプリがsession_regenerate_id関数を呼んでセッションIDを再生成していても、ohgaki.netに設定されたクッキーが優先されてしまうため、新しいセッションIDは利用されず、攻撃者にとって既知のセッションIDでセッションを初期化してしまうからです。

対策がとられていない場合、ユーザは攻撃されていることにまったく気づきません。攻撃者が設定したセッションIDでセッションを初期化しログインすると、セッションの乗っ取りが可能になります。

パスを利用する方法

まず最初にセッションモジュールがどのようにクッキーのパスパラメータを設定しているか説明します。セッション開始時やsession_regenerate_id関数が設定するセッションID用クッキーのパスはphp.ini設定により決まります。この設定は、session_set_cookie_params関数で変更することも可能です。デフォルト設定のパスは⁠/⁠に設定されています。

複数のアプリケーションを同じドメイン名(DNS名)で運用している場合も多いと思います。例えば、

  • http://www.ohgaki.net/blog なら ブログ
  • http://www.ohgaki.net/wiki なら Wiki

などのようにパスでアプリケーションを別けているサイトも多いと思います。

どのパスのクッキーを最初に送ってくるかはブラウザ次第ですが、深いパスのクッキーを最初に送信するブラウザがほとんどです。つまり、もし

  • http://www.ohgaki.net/wiki/

のアプリケーションにJavaScriptインジェクション脆弱性があった場合、Wikiアプリケーションの安全性が脅かされるだけでなく

  • http://www.ohgaki.net/blog/

で運用しているアプリケーションの安全性も損なわれます。

それはブラウザが⁠/wiki/⁠パスはもちろん、⁠/blog/⁠パスにもクッキーを設定することを許しているからです。ブラウザはパスが実在しようとしまいと関係なしに、パス設定付きのクッキーを許しています。

既に説明した通り、ほとんどのブラウザは深いレベルのパスのクッキーを最初に送信します。つまり、デフォルトのセッションの初期化やsession_regenerate_id関数は⁠/⁠にクッキー設定をするので、攻撃者が設定した攻撃用のセッションIDを上書きすることができません。

session_regenerate_idのdelete_old_sessionオプション

session_regenerate_id関数はオプションで、セッションデータを削除してから新しいセッションIDを設定するように動作させることができます。

session_regenerate_id(ture);

と呼び出すと、新しいセッションIDを設定する前にsession_destory関数を呼び出します。しかし、この動作は、PHPの内部でセッションデータを破棄するだけなので、セッションアダプション脆弱性を利用した攻撃に何の意味もありません。

クッキーファイルを利用する方法

この方法がセッションアダプションに脆弱なアプリケーションに対して最も強力です。多くのブラウザはサーバが送信してきたクッキーをファイルに保存しています。このファイルに直接アクセスでき、読み取り専用にできればクッキーの値は変更できなくなります。つまり、サーバ側でいかなる手段をとっても、読み取り専用のクッキーファイルを利用して攻撃された場合は防御の方法がありません。

セッションアダプション攻撃からの防御

本来、セッションモジュール自体がセッションアダプションに脆弱であってはなりません。セッションモジュールへのパッチは筆者が把握しているだけで2回、PHPセキュリティレスポンスチームに提案されています。

少なくとも2005年の時点で、PHPプロジェクトに対してStefan Esser氏がsessionモジュールのセッションアダプション脆弱性を修正する提案をパッチ付きで行っています。パッチなしの改善提案はそれより数年前に行われています。筆者自身も2008年8月のPHP4セキュリティパッチ配布停止を機会に、Stefan Esser氏のパッチをベースとしたセッションアダプション脆弱性修正の提案をパッチ付きで行っています。

しかし、残念ながらPHPプロジェクトのセキュリティレスポンスチームの理解不足により、この脆弱性の危険性は正しく理解されていません[2]⁠。

パッチで対処する方法

筆者のWikiにてセッションモジュールのセッションアダプション脆弱性を修正するパッチを公開しています。

PHP5.2.x用のパッチは筆者が修正/機能追加したもの、PHP 4.4.9用のパッチは枡形氏が修正したものを公開しています。

執筆時点(4/22)でPHPのCVSの開発版にも、セッションアダプションを修正するパッチは適用されていません。当然ですがPHP 5.3にもセッションアダプション脆弱性は残ったままになっています。

筆者も開発に参加しているMomonga ProjectのMomonga Linux 5のPHPパッケージにはセッションアダプションを修正するパッチを適用済みですが、筆者の知る限りこの種のパッチを適用しているディストリビューションはありません。

PHPスクリプトで対処する方法

幸いなことに、最後の読み取り専用のクッキーファイルを利用した攻撃以外であれば、PHPスクリプトで対処可能です。しかし、筆者の作ったアプリケーション以外でこの対処を行っているアプリケーションはまだ見たことがありません。オープンソースのPHPアプリケーションはほぼ間違いなく、セッションアダプション脆弱性を利用した攻撃に脆弱です。session_regenerate_id関数でさえ、ログイン処理時に呼び出していないアプリケーションも多数あります。

クッキーベースのセッション管理でsession_regenerate_id関数を呼び出しても、セッションアダプション脆弱性が問題となる原因は、ブラウザが複数のクッキーを設定できてしまうことにあります。

つまり、セッションIDを再生成する際に、設定可能なクッキーすべてを予め削除しておけば、ほとんどのセッションアダプション脆弱性を利用した攻撃を防げます。

セッションアダプション対策の副作用

パッチによるセッションアダプション対策を行うと、DoSが可能になる副作用が発生します。ログイン処理時に初期化済みのセッションID用のクッキーをサーバが送信しても、そのクッキーよりも攻撃者が設定したクッキーが優先され、何度ログインを試みてもログインできない状態になります。

このDoS攻撃を防ぐためには、すべてのURLでセッション管理用のクッキードメインとパス以外の、すべてのセッション管理用のクッキー名を持つクッキーを削除しなけばなりません。

「PHPスクリプトで対処する方法」で解説した方法を利用しただけでは、ログイン処理を行っているURLしか、対処できません。

完全にこのDoSを防ぐためには、面倒でもすべてのURLで不要なセッション管理用クッキーの削除を行わなければなりません。

まとめ

本来、セッション管理機構はセッションアダプションに脆弱であってはなりません。筆者も機会があるごとにPHPセキュリティレスポンスチームに、セッションアダプション脆弱性を修正するように勧めますが、当面この問題が修正されることは無いと考えてください。

修正されるまでは、ログイン処理時にsession_regenerate_idを呼び出す前に、ログインスクリプトが実行されているURLで設定可能なすべてのセッションID用のクッキーを削除するようにしてください。

おすすめ記事

記事・ニュース一覧