セッション管理の基礎
セッションのセキュリティ
セッションモジュールは、セッションに保存された情報を 作成したユーザーだけが見られることを保証できません。 セッションに紐付けられた値によっては、 セッションの機密性を守るために追加の対策が必要です。
セッションに保存されているデータがどれくらい重要かを評価し、 さらなる防御策を展開すべきかもしれません。 通常、そうしてしまうとコストが高く付きます。 たとえば、ユーザーの利便性を損なうことが挙げられます。 たとえば、ユーザーを単純なソーシャルエンジニアリングから守るためには、 session.use_only_cookies を有効にする必要があります。 この場合、クッキーを無条件にクライアント側でも有効にしなければなりません。 さもないとセッションが動作しないからです。
既存のセッションIDをサードパーティに漏らす方法は複数あります。 JavaScriptインジェクション、セッションID を URL に埋め込む方法、 パケット盗聴、デバイスへの物理アクセス、などです。 セッションIDが漏れてしまうと、 サードパーティがその特定のIDに紐付けられた全てのリソースに アクセスできるようになってしまいます。 既存のセッションIDを漏らす最初の方法は、URL でセッションIDを持ち回すやり方です。 もし外部サイトやリソースへのリンクが存在したとすると、 セッションIDを含むURLが、外部サイトのリファラログに保存されるかもしれません。 ふたつめは、より積極的な攻撃者がネットワークトラフィックを盗聴しているかもしれません。 ネットワークトラフィックが暗号化されていないと、 セッションIDがネットワーク越しにプレーンテキスト内に流出してしまいます。 防御策は、SSL/TLS をサーバー側で実装し、ユーザーがそれを使うように強制することです。 HSTS も、セキュリティを向上させるために使えるはずです。
注意: HTTPS でさえも、あらゆる時に機密データを保護することはできません。 たとえば、CRIME や Beast 脆弱性が、攻撃者にデータを読めるようにしてしまうかもしれません。 また、多くのネットワークは HTTPS の MITMプロキシ を監査の目的で採用しています。 攻撃者も、そうしたプロキシをセットアップしているかもしれません。
セッションアダプションを防ぐためのセッション管理
PHP のセッションマネージャーは、 現状ではデフォルトでセッションアダプション攻撃が可能です。 アダプション攻撃が可能なセッションマネージャーは、 追加のリスクを背負うことになります。
session.use_strict_modeを有効にし、かつセッションの保存ハンドラがサポートしている場合、
初期化されていないセッションIDは拒否され、新しいものが作成されます。
これによって、攻撃者がユーザーに既知のセッションIDを強制的に使わせることを防止できます。
攻撃者はセッションID付きのリンクをペーストしたり、
そうしたリンクを含んだメールを送るかもしれません。
たとえば http://example.com/page.php?PHPSESSID=123456789
のようなものです。
session.use_trans_sid が有効な場合、
ユーザーは攻撃者が提供したセッションIDを使ってセッションを開始してしまいます。
session.use_strict_mode は、
こうしたリスクを軽減します。
ユーザー定義の保存ハンドラも、セッションIDの検証を行うことで 厳格なセッションモードをサポートできます。 保存ハンドラを定義した全てのユーザーは、セッションIDの検証を実装すべきです。
セッションIDクッキーは、domain, path, httponly, secure, そして PHP 7.3 以降では、SameSite 属性とともに設定できます。 これは、ブラウザで定義されたものが優先します。 この優先される仕組みを使い、攻撃者は永久に使えるセッションIDを設定できてしまいます。 session.use_only_cookies はこの問題を解決できません。 session.use_strict_mode を使うとこのリスクを軽減できます。 session.use_strict_mode=On にすると、 初期化されていないセッションIDは拒否されます。
注意: session.use_strict_mode はアダプション可能なセッション管理機構のリスクを軽減しますが、 攻撃者は自らが生成したセッションIDを、 初期化済みセッションIDとして使わせることが出来てしまいます。 たとえば JavaScriptインジェクション攻撃です。 この攻撃はこのマニュアルで推奨する方法で軽減できます。 このマニュアルに従い、開発者は session.use_strict_mode を有効にし、タイムスタンプベースのセッション管理を行い、 推奨する手続きを使って session_regenerate_id() を使い セッションIDの再生成を行うべきです。 開発者が上記全てに従えば、攻撃者が生成したセッションIDは、結局は削除されます。 有効期限が切れたセッションにアクセスされたときのために、 開発者は全てのアクティブなユーザーのセッションデータを保存すべきです。 この情報は後に行われる調査とも関係があります。 ユーザーは全てのセッションから強制的にログアウトさせられるべきです。 つまり、再認証させるべきです。 こうすることで、攻撃者が盗まれたセッションを悪用することを防止できます。
有効期限が切れたセッションにアクセスすることが、 必ずしも攻撃であることを示しているとは限りません。 不安定なネットワークや、 アクティブなセッションをすぐに削除してしまったりすることが原因で、 正当なユーザーが、有効期限が切れたセッションにアクセスすることがあるからです。
PHP 7.1.0 以降、session_create_id() が追加されました。 この関数は、セッションIDの前にユーザー定義のIDをつけることで、 全てのアクティブなユーザーのセッションに効率的にアクセスできます。 session.use_strict_mode を有効にすることが重要です。有効にしないと、 悪意のあるユーザーが悪意のあるセッションIDを他のユーザーに設定できてしまいます。
注意: PHP 7.1.0 より前のバージョンのユーザーは新しいセッションIDを生成する のに CSPRNG、 つまり /dev/urandom や random_bytes() やハッシュ関数を使うべき です。 session_create_id() には衝突を検出する機能があり、 セッションの INI 設定に従ってセッションIDを生成します。 session_create_id() の利用が好ましいです。
セッションID の再生成
session.use_strict_mode は良い軽減策ですが、十分ではありません。開発者はセッションのセキュリティのために session_regenerate_id() を同時に使うべきです。
セッションIDを再生成すると、盗まれたセッションIDを使われるリスクが減らせるので、 session_regenerate_id() 関数は定期的に呼び出されなければなりません。 たとえば、セキュリティに敏感な内容を扱っているのなら、 セッションIDを15分毎に再生成するようにしてください。 セッションID が盗まれた場合でも、正当なユーザーと攻撃者のセッションは両方時間切れになります。 言い換えれば、ユーザーだろうと攻撃者のアクセスだろうと、 期限切れのセッションにアクセスしたとしてエラーになるのです。
セッションID はユーザーの権限が上昇したときは再生成されなければ なりません。 たとえば認証が成功した後などです。 session_regenerate_id() は、 認証情報を $_SESSION に設定する前に必ずコールされなければなりません。 (session_regenerate_id() は タイムスタンプなどを保存するために、現在のセッションにセッションデータを自動的に保存します) 必ず新しいセッションだけに、認証済みのフラグが含まれるようにしてください。
開発者は session.gc_maxlifetime によるセッションの期限切れに依存しては いけません。 攻撃者は、認証済みのセッションも含めて、期限切れを防いで悪用を続けるため、 ターゲットのセッションIDに定期的にアクセスしている可能性があるからです。
そのかわりに、開発者はタイムスタンプベースのセッション管理を実装しなければなりません。
セッションマネージャーはタイムスタンプを透過的に管理できますが、
この機能はまだ実装されていません。
古いセッションデータは GC されるまで保持されなければなりません。
同時に、開発者は期限切れになったセッションデータが削除されることを保証しなければなりません。
たとえば、session_regenerate_id(true);
と
session_destroy() をアクティブなセッションに対して同時に呼んでは絶対にいけません。
これは矛盾したように聞こえますが、これは必須条件です。
session_regenerate_id() は、 デフォルトで期限切れのセッションを削除 しません。 期限切れの認証済みのセッションが存在するかもしれません。 開発者は期限切れのセッションは、誰からも消費されないようにしなければなりません。 期限切れのセッションデータへのアクセスを、タイムスタンプを使うことで禁止すべきです。
アクティブなセッションを突然削除すると、望まない副作用が起きます。 Webアプリケーションへの同時接続が起きた かつ/または ネットワークが不安定な場合に、セッションが突然消える可能性があります。
アクティブなセッションが突然消えてしまうと、 悪意のあるアクセスの可能性を検出することができなくなります。
期限切れのセッションを突然削除するのではなく、 開発者は短い期限切れの時間(タイムスタンプ)を $_SESSION に設定し、セッションデータにアクセスすることを自分で禁止すべきです。
開発者は session_regenerate_id() を呼び出した後すぐに、 古いセッションデータへのアクセスを禁止してはいけません。 その後の段階で禁止しなければなりません。 たとえば、有線ネットワークのような安定したネットワークは数秒後、 無線LAN や携帯電話のような不安定なネットワークの場合は、2, 3分後です。
ユーザーのアクセスが期限切れのセッションだった場合、 アクセスを拒否するべきです。攻撃されている可能性があるすべてのユーザーから、 認証済みのステータスを削除することも推奨します。
session.use_only_cookies と session_regenerate_id() を適切に使うことで、 攻撃者によって設定された、削除できないクッキーを伴うDOSが発生することがあります。 この場合、開発者はユーザーに Cookie を削除させ、 セキュリティ上の問題に晒されていることを教えることができます。 攻撃者は、脆弱なWebアプリケーションや ウイルスに感染したブラウザのプラグイン、 物理的に侵害されたデバイスなどを経由して悪意のあるCookieを設定している可能性があります。
DOS のリスクを誤解しないでください。 session.use_strict_mode=On は一般的なセッションIDのセキュリティには必須です! 全てのサイトが session.use_strict_mode を有効にすることをお勧めします。
DOS はアカウントが攻撃されている場合にだけ発生する可能性があります。 アプリケーションに存在する JavaScript インジェクションの脆弱性がもっともよくある原因です。
セッションデータを削除する
期限切れのセッションデータは、アクセス不能にし、削除しなければなりません。 現状のセッションモジュールはこの機能をうまく扱えません。
期限切れのセッションデータは、できるだけ早く削除すべきです。 しかしながら、アクティブなセッションは、すぐに削除してはいけません。 これらの要求を満たすためには、 開発者はタイムスタンプベースのセッション管理を自分自身で実装する必要があります。
期限切れのタイムスタンプを $_SESSION に設定し、管理します。 期限切れのセッションデータへのアクセスは禁止します。 期限切れのセッションデータへのアクセスが検出された場合、 そのユーザーのセッションから認証済みのステータスを全て取り除き、 強制的に再認証させることをおすすめします。 期限切れのセッションデータへのアクセスは、攻撃されている可能性があります。 これを実現するために、 開発者はユーザーごとに全てのアクティブなセッションを追跡しなければなりません。
注意: 期限切れのセッションへのアクセスは、ネットワークが不安定だったり、 Webサイトへの同時アクセスも原因で起こりえます。 たとえば、サーバーは新しいセッションIDを Cookie 経由で設定しようとしたが、 接続が失われたため Set-Cookie のパケットが クライアントに届かなかったりする可能性があります。 ある接続で、session_regenerate_id() によって新しいセッションIDが発行されたけれども、 別の同時接続が、まだ新しいセッションIDを受け取っていない可能性もあります。 よって、開発者は期限切れのセッションへのアクセスを後の段階で禁止しなければなりません。 すなわち、タイムスタンプベースのセッション管理が必須なのです。
まとめると、セッションデータは session_regenerate_id() や session_destroy() を使って破棄してはいけません。 むしろ、セッションデータへのアクセスを制御するためにタイムスタンプを使わなければなりません。 session_gc() 関数に、 セッションデータストレージから、期限切れのデータを削除させましょう。
Session とロック
セッションデータはレースコンディションを避けるため、デフォルトでロックされます。 ロックを掛けることは複数のリクエスト間でセッションデータの一貫性を保つため、必須です。
しかしながら、セッションをロックしてしまうと、
攻撃者がDOS攻撃に悪用する可能性があります。
セッションをロックすることによるDOS攻撃のリスクを軽減するためには、
ロックを最小限にすることです。
セッションデータを更新する必要がないときは、読み取り専用のセッションを使うようにします。
session_start(['read_and_close'=>1]);
$_SESSION を更新した後は、session_commit() を使って
できるだけ早くセッションを閉じるようにします。
現状のセッションモジュールは、 セッションがアクティブでない時に $_SESSION が変更されたかを検知 しません。 セッションがアクティブでない時に $_SESSION を変更しないことは、開発者の責任です。
アクティブなセッション
開発者は、ユーザーごとにアクティブなセッションを追跡し続けるべきです。 そして、アクティブなセッションの数がどれくらいあるかや、 どのIP(や地域) からかとか、どのくらいの期間アクティブなのか、などです。 PHP はこれらを追跡しません。開発者が行うことが想定されています。
これを実装するやり方はたくさんあります。 ひとつの可能な実装は、データベースをセットアップして必須のデータを追跡し、 関連するあらゆる情報を保存することです。 セッションデータは GC されるので、開発者はアクティブなセッションのデータベースが一貫性を保つように GC されたデータに注意を払わなければなりません。
一番簡単な実装は、"ユーザーIDをセッションIDの前に付ける" ことで、 必須の情報を $_SESSION に保存することです。 多くのデータベースは文字列の prefix を選択するのに良いパフォーマンスを発揮します。 開発者は session_regenerate_id() と session_create_id() をこの目的で使うこともできます。
秘密のデータを prefix に絶対に使わないでください。 ユーザーIDが秘密の場合、hash_hmac() を使うことも検討しましょう。
session.use_strict_mode はこの場合に必須です。必ず有効にするようにしてください。 そうしなければ、アクティブなセッションデータベースが侵害される可能性があります。
タイムスタンプベースのセッション管理は、時間切れのセッションへのアクセスを検知するのに必須です。 時間切れのセッションへのアクセスが検知された場合、 そのユーザーの全てのアクティブなセッションから認証済みのフラグを削除すべきです。 こうすることで、攻撃者が盗んだセッションを悪用し続けることを防げます。
Session と自動ログイン
開発者は有効期間が長いセッションを、自動ログインに使ってはいけません。 なぜなら、セッションが盗まれるリスクが増えるからです。 自動ログイン機能は、開発者が実装すべきです。
setcookie() を使い、 セキュアなワンタイムハッシュを自動ログインのキーとして使います。 SHA-2 より強いセキュアなハッシュを使います。 たとえば、 random_bytes() や /dev/urandom で生成したランダムデータを SHA-256 またはそれより強いハッシュ関数と一緒に使います。
ユーザーが認証されていなかったら、ワンタイムの自動ログインキーが有効かどうかをチェックします。 もし、キーが正しければ、ユーザーを認証し、新しいセキュアなワンタイムハッシュキーを設定します。 自動ログインキーを使うのは一度きりです。つまり、再利用してはいけません。 常に新しいものを生成するようにします。
自動ログインのキーが長期間有効な認証キーである場合、 できるだけ手厚く保護すべきです。 path/httponly/secure/SameSite クッキー属性をセキュアにするために使います。 つまり、自動ログインキーを必要ないのに送信しないでください。
開発者は自動ログインを無効にする機能や、 不要な自動ログインのキーを削除する機能を実装しなければなりません。