(Last Updated On: 2018年10月12日)

HTTPセッション管理はWebセキュリティの中核と言える機能です。Webセキュリティの中核であるHTTPセッション管理に設計上のバグがある事は少なくありません。今回のエントリはPHP Webアプリ開発者ではなく、主にWebフレームワーク側の開発者、つまりPHP本体の方に間違いがあるという話しです。Webアプリ開発者の回避策も紹介します。

まずセキュリティの基本として「入力のバリデーションを行い、正当な入力のみを受け入れる」があります。しかし、PHPに限らず多くのセッション管理機構は当たり前の「入力のバリデーションを行い、正当な入力のみを受け入れる」を行っていません。セッションIDの再生成(リセット)も不完全な物が多いと思います。

参考:

セッション管理機構が入力バリデーションを行わない仕様の問題

全ての外部入力はバリデーションされなければならない。これはセキュリティ対策として最も有効とされている基本的な対策です。Webセキュリティの中核であるセッション管理機構が送信されたセッションIDをバリデーションしない仕様に正当性はありません。セッション管理機構が入力バリデーションを行わないと次のような問題が発生します。

  • 未初期化のセッションIDが送信されても、そのIDを受け入れてしまう
  • アプリがセッションIDの再生成を忘れている場合、簡単にアカウントを乗っ取れる
  • 攻撃者が好き勝手にセッションIDを作れる

未初期化のセッションIDを受け入れると、埋込み型(URL・ページ埋め込み)のセッションID管理の場合はブックマークなどでセッションIDが固定化します。クッキーを利用している場合も、固定化を行えます。クッキーのpath, domain, httponly属性の利用、データベースストレージを使った恒久的なJavaScriptインジェクションで恒久的な固定化が行えます。

アプリがセッションIDの再生成を忘れている場合、セッションIDが固定化する事を利用して簡単にアカウントが乗っ取れてしまうことも問題です。埋込み型のセッションID管理の場合、ソーシャルエンジニアリングを使った罠ページにセッションIDを埋め込んだURL、例えば

http://target.example.com/?PHPSESSID=123456789

をクリックさせるだけです。アプリがクッキーを利用したセッションID管理を利用し、JavaScriptインジェクションに脆弱な場合、永久的な乗っ取りも可能です。攻撃者にとって都合の良い仕様です。

未初期化のセッションIDでも受け入れるセッション管理なら、好き勝手にセッションIDを作れます。攻撃者は予め初期化済みのセッションIDを用意する必要さえありません。これも攻撃者にとって都合の良い仕様です。

Webサイトの利用者もWebサイトの開発者も、セッション管理機構が管理していないセッションIDに対して新しいセッションIDを設定しても、困る事はありません。セッションIDをバリデーションしない仕様は攻撃者を助ける為の仕様と言えます。

PHPの場合、5.5.2からuse_strict_session=Onを設定するとセッションIDのバリデーションが行われるようになります。もしかすると5.6からuse_strict_mode=Onがデフォルトになるかも知れません。5.6は間に合わないかも知れませんが、5.7では確実にデフォルトになっていると思います。

 

セッション管理機構が信頼性のあるセッションID再生成を行わない問題

セッション管理機構にはセッションIDを再生成(リセット)する機能が付いています。もしこの機能がない場合、そのセッション管理機構は完全に壊れている、と言っても構わないくらい重要な機能です。しかし、この重要な機能が信頼性のある形で正しく実装されていない事が多いようです。

セッションIDを再生成した場合、古いセッションデータは確実に削除されなければなりません。しかし、このような動作になっていない場合があります。

例えば、PHPではsession_regenerate_id()がセッションIDの再生成を行う関数ですがデフォルトでは古いセッションIDのデータを削除しません。これはブラウザが同時接続で複数のリソースにアクセスしている場合、古いセッションIDのデータが消えるとアプリが正常に動作しない場合がある為こうなっています。

この仕様では、セッションIDが盗聴やJavaScriptインジェクションで盗まれていても、正規の利用者のセッション、攻撃者のセッション、どちらでIDの再生成を行っても両者ともアクセスし続けられます。正規の利用者もWebサイト側も不正利用を識別できません。アプリで最大のログイン時間を越えると再認証を求める仕様にするのは、このようなリスクを軽減する為です。セッションだけ盗んだのであればパスワードは分かりません。(パスワードを表示してしまう設計ミスが無い限り。。)

これを回避するにはセッションID再生成を行った後に、不要になったセッションデータをしばらく経ってから消し、不正アクセスを検出する動作が必要です。PHPの場合、このような動作を行うコードはそれほど難しくありません。しかし、セッション管理ですべき基本的な事は、セッション管理機構が当たり前にやる方が良いと考えています。仕様上のバグと言って良いでしょう。

注:PHPのセッション管理を知らない方向け。session_regenerate_id()をオプション無しで呼んでも有効期限切れでGCが動作すると削除されます。

セッションIDの再生成を行った場合に古いセッションデータを直ぐに削除してしまう仕様の場合、非同期に同時接続を行うWebクライアントの仕様を理解せずに設計した仕様上のバグと言えます。時々一部のイメージなどが正常に表示されない事があるサイトがありますが、もしかするとセッションIDの再生成が適切に行われていないのかも知れません。

 

セキュリティを無視した仕様

PHPに限らず、セキュリティの基本を無視した設計になっているセッション管理機構が当たり前のようです。勿論、Web開発者のコードでこれらの問題を解決する方法はあるのですが、Webセキュリティの中核であるセッション管理機構で実装できるのにWebアプリ開発者任せにして実装しない、ことは間違いだと考えています。

セッション管理機構がセッションIDのバリデーションを行わない仕様は、ユーザー(Web開発者、Web利用者)の為の仕様ではなく攻撃者の為の仕様、になっています。セッション管理機構の開発者がどちらの味方になるべきか?議論の余地はありません。Web開発者が安全に使えば良い、はセッション管理機構の開発者としては境界防御、多層防御、フェイルセーフ対策を無視した考えです。後で説明しますが仕様として致命的な欠陥でもあります。

セッションIDを再生成した時に古いセッションデータを自動的かつ確実に削除しない仕様も、セキュリティの中核である機能の仕様としてはお粗末、と言われても仕方ないと思います。Web開発者が回避することも可能です。しかし、セッション管理機構で実装できるにも関わらず、実装しない事に正当性はないでしょう。後ほど説明するWeb開発者による回避策は難しいものではありませんが、単純と言えるものでもありません。

ネットワークにはネットワークの、アプリケーションにはアプリケーションのセキュリティ担当範囲があるように、セッション管理機構にはセッション管理機構の担当範囲があります。やるべき事をやるべき場所で適切に処理するのがセキュアなシステム設計・開発の基本です。やるべき事の定義が異なる事が問題となる場合が多いです。これに関してはまとめで書きます。

 

利用者はどうすべきか?

ここでの利用者とはセッション管理機構を利用しているWeb開発者を指しています。今現在、無いものなら自分で実装するしかありません。セッション管理機構の実装を理解した上で、安全・確実なセッション管理を行うようにしなければなりません。セッション管理機構によるセッションIDのバリデーションがあっても、そもそも頼るべきセキュリティ対策ではありません。セッション管理機構がバリデーションしていても攻撃者が初期化したセッションIDである可能性があります。これはどうしようもありません。(緩和策がない訳ではありませんが、セッション管理のコア機能としての実装はあまり現実的ではありません)この為、認証情報の更新など、セキュリティ上重要な場所でセッションIDを再生成する必要があります。一定時間ごとにセッションIDを再生成する事も忘れないで下さい。

セッション管理機構が初期化したセッションであるかどうかは、フラグを立てる事で確認可能です。PHPの場合、フラグが無ければsession_regenerate_id()を呼んだ後にセッション管理機構が初期化した事を意味するフラグを立てます。PHP5.5ユーザーなら単純にsession.use_strict_mode=Onに設定するだけOKです。他に何もする必要はありません。

session_regenerate_id()で要らなくなった古いセッションの確実な削除にもフラグを利用します。session_regenerate_id()を呼ぶ前に、有効期限を$_SESSION配列に保存します。その後、session_commit()で保存します。session_start()でセッションを再開し、session_regenerate_id()でセッションIDを再生成し、$_SESSION配列から有効期限フラグを削除します。アプリのsession_start()を呼び出す部分で有効期限フラグをチェックし、有効期限切れの場合はセッションを破棄します。有効期限切れのセッションからのアクセスは何らかの攻撃の可能性が高いのでエラーを記録、レポートして終了します。PHP 5.6からこのようなコードは書かなくても、session_regenerate_id()を単純に呼ぶだけで、これらのほぼ全てが行えるようにしたかったのですが、残念ながら少し無理そうな感じです。既にパッチは作ってあるのですが。。。

上記のようにsession_regenerate_id()を利用した場合、正規Web利用者のセッションIDが再生成されても、攻撃者のセッションIDが再生成されても、Webサイト側では問題があった事を検出できます。攻撃者のセッションIDが再生成された場合、正規Web利用者のセッションが使えなくなってしまうので、正規Web利用者が直接問題に気付く可能性もあります。正規Web利用者のセッションIDが再生成された場合も、攻撃者に保護機能があることが分かります。それ以上の攻撃を止める可能性もあります。

iframeを使用したり、複数のタブでサイトを開いていると問題の可能性がある、攻撃の検出ができないなどの問題もありますが、session_regenerate_id()をHTML文書のリクエストの場合にだけ呼び、古いセッションデータを即座に削除する方法もあります。これで問題のないアプリも多いと思います。

上記以外にもフラグが必要になる場合があります。これはWebアプリ開発者が管理するしかありません。例えば、以下のような場合です。

  • 認証前にショッピングカートに商品を入れる

ショッピングカートのように認証前でも、できれば保護したい情報を扱いたい場合もあります。これはよく言われている「認証時にセッションIDを再生成する」対策だけでは不十分です。セッションIDのバリデーションだけでも不十分です。確実に保護する為には、セッションID再生成済みフラグが無い場合にはセッションIDを再生成し、セッションID再生成済みフラグをセッションに保存します。この場合、古いセッション情報を確実に削除する必要性はあまり無いはずなので、session_regenerate_id()を呼んだ後、$_SESSION配列にフラグを保存し、確認するだけで良いでしょう。

注:認証しているのではないので、攻撃者が予めフラグが立っているセッションを利用させている場合、これでも不十分です。

 

開発者はどうすべきか?

ここでの開発者とはセッション管理機構を開発しているフレームワーク開発者を指しています。セッション管理機構の利用者はこうするべき、という話は当然あって良いのですが、自分が開発している部分(セッション管理機構)で行えるセキュリティ対策で仕様・性能・技術的に問題なく実装できる対策がある場合は取り入れるべきです。リスクの緩和策はセキュリティ対策です。境界防御、多層防御、フェイルセーフも重要なセキュリティ対策です。特にセッション管理はWebセキュリティの中心であり尚更です。攻撃者にとって都合の良い仕様で作る必要性はありません。セキュリティの重要部分であるセッション管理機構では、できる限りの対策を行った上で、カバーしきれない部分をユーザー(Web開発者、Web利用者)の責任だとするべきです。

初期化済みのセッションIDでさえ危険かも知れないのに、未初期化のセッションIDをそのまま受け入れるのは問題外だと思います。未初期化のセッションIDを受け入れる仕様には、明らかに致命的な問題があります。

例えば、セッションID埋め込みをサポートするセッションID管理を利用し、WikiなどにセッションID埋込みのURLを貼り付けた場合、複数の人がそのURLにアクセスするかも知れません。セッションIDのバリデーションがある場合、埋め込まれたセッションIDの有効期限が切れているか、ログオフで削除されていれば、複数の人がアクセスしても別々のセッションIDが生成されます。URLに貼り付けたセッションIDと同じセッションIDを人為的に生成する事も不可能です。未初期化のセッションIDを受け付ける仕様では「セッションIDは個別のHTTPセッションを識別するID」とする定義に合致しません。普通の使い方ですが問題が発生しています。仕様上のバグと言えます。

クッキーのみのセッション管理は攻撃者がセッションIDを設定することになりますが、同様の理由でバグと言えます。攻撃者が設定したかも知れないIDをそのまま使うと、HTTPセッションを識別するIDでない可能性があるIDをそのまま使う事になります。セッション管理機構として欠陥です。欠陥は修正し、その上でWeb開発者に攻撃者が、初期化済みのIDインジェクションが可能であること(JavaScriptなどで設定)、httponlyを設定しても無効になってしまう場合があること、などに注意を呼びかければよいでしょう。

JavaScriptインジェクションによるセッションIDの恒久的な固定化は様々な方法(Cookieの仕様、データベースへの保存、HTML5のストレージ、など)を使って可能です。セッションIDのバリデーションがある場合、初期化済みのセッションでしか攻撃できなくなります。攻撃者は設定したセッションIDを恒久的に保持しなければ攻撃できません。Wikiの例と同様にセッションIDをバリデーションするセッション管理機構であれば、一度攻撃に使っているIDが削除されれば、攻撃者は二度と同じIDを使えません。

セッションIDのバリデーションが無い場合、攻撃者は何もする必要はなく被害者が罠に掛かっているか確認するだけで済みます。

 

まとめ

HTTPセッション管理が間違っている部分

  • セキュリティ対策の基本を無視している
  • セッションIDのバリデーションが行われていない
  • セッションIDが接続を識別する一意なIDとする定義を満たしていない
  • セッションIDの再生成(リセット)で古いセッションデータが確実に削除されない
  • セッションIDの再生成(リセット)で古いセッションデータを即時に削除する
  • 結果としてユーザー(Web開発者、Web利用者)を助けるのではなく、攻撃者を助けている

セキュリティ対策の認識としてよく間違えられている事に、緩和策(Mitigation)がセキュリティ対策でない、と誤解されている事があります。合意がとれている対策に異論を持つ事は自由ですが、緩和策は国際的に合意が取れているセキュリティ対策です。境界防御、多層防御、フェイルセーフも基本中の基本の対策です。Webセキュリティの核心部分でもあるHTTPセッション管理に不正IDの注入を防止する境界防御、緩和策、多層防御、フェイルセーフが必要ない、と私は考えていません。

入力を確実にバリデーションする、出力を確実に安全にする、を確実に行えばアプリケーションのセキュリティは間違いなく向上します。HTTPセッション管理の場合、入力はセッションIDのバリデーション、出力は安全かつ確実なセッションデータの削除が該当します。

セッション管理機構に、緩和策を取り入れる、入力と出力を確実に行う、セキュリティ対策の基礎を考え仕様を作っていれば、ここに書いたような余計な事は全く気にせず済んだはずです。Webアプリのセキュリティも、効果的な緩和策の導入、入力と出力を確実にする事を考えて作れば、余計な事を考える事が少なくなります。

入出力に関するセキュリティ対策については、最初に参考として記述した発者は必修、SANS TOP 25の怪物的なセキュリティ対策エンジニア必須の概念 – 契約による設計と信頼境界線を参照してください。

セキュリティをより簡単&より確実に!私はこの方針でPHP開発に協力しています。ここに書いている機能で準備済みの物が、PHP 5.6で提供できないかも知れない事は残念です。しかし、そのうち入れます!

追記:まだ諦めては居ないのですが、正しく&正確にタイムアウトを管理することの重要性を開発者が理解できていなかったので1度目のトライでは失敗しています。

https://wiki.php.net/rfc/precise_session_management?rev=1453806448

上記のURLにはPHPによる実装例も記載しています。参考にどうぞ。

参考:

投稿者: yohgaki