at posts/single.html

PHPのセッション管理の注意点メモ (1)

先日の日記に書いた Ruby でのセッション管理に続けて、 PHP でのセッション管理を試している。 ちょっと調べてみたところ、いくつか注意することがあるので、メモとして残しておく。

※ あくまで個人メモです。これだけをやればいい訳じゃないので注意。 なお、セキュリティについての詳細な情報は「PHP と Web アプリケーションのセキュリティについてのメモ」が参考になります。

(1) URLにセッションIDが含まれてしまう(場合がある)

PHPでは、Cookie が使えない端末(携帯など)向けに、セッションIDをURLに埋め込む機能がある。 この機能が有効だと、そのページ(セッションIDを含むURLのページ)から他のサイトへ移動したときに、リファラ経由でセッションIDが他のサイトに伝わってしまう。 php.ini に下記を設定することで、セッションIDを URL に出力しなくなる(デフォルトはこの設定になっているらしい)。

session.use_trans_sid = 0

Cookie 非対応の携帯向けアプリだとこうはいかないけど、少なくとも PC 向けなら無効にしたほうがいい。

参考: SecurIT-Advisory 2005-001: URLに埋め込むIDに頼ったセッション管理方式の脆弱性(2)

(2) URLに付与されたセッションIDを受け付けてしまう

(1) と関連するけど、URLのクエリストリングとしてセッションIDが付与されている場合に、設定によってはサーバはこのセッションIDを受け付ける。 何故か session.use_trans_sid を 0 に設定しても、この動作は変わらない。

さらに、対応するセッションIDがサーバ側に存在しない場合は、そのセッションIDで新規にセッションを発行する。 例えば、利用者が以下のクエリを含んだURLにアクセスすると、サーバ側に1234というセッションIDが登録されていなくても、セッションID 1234番が新規に発行される。

http://examle.com/test.php?PHPSESSID=1234

これには、以下の2つの挙動が含まれている。

  1. (Cookieではなく) URLに付与されたセッションIDを受け付ける
  2. サーバ側に存在しないセッションIDを受け付ける

利用者がセッションIDを好きに決めることの何が問題なの?と思っちゃうかもしれない。 でも、 (イタズラ好きな) 利用者が他の利用者にこのセッションIDを送りつけることができるとなると、話は変わってくる。 先ほどの example.com が登録制のサイトだとする。

  • イタズラ好きな利用者は、 PHPSESSID=1234 が含まれる上記のリンクを大勢の人が見る掲示板などに貼り付ける。
  • そのリンクをクリックした人は 登録制のサイトにて1234番のセッションIDを持つことになる。
  • この1234番という番号は、イタズラ好きな利用者が知っている番号になる。

ある利用者のセッションIDが他の人に知られることでの被害の程度は様々だけど、まぁ望ましい状態じゃない。 php.ini に下記の設定をすることで、サーバは Cookie に含まれるセッションIDのみを受け付けるようになる(デフォルト値は0?)。

session.use_only_cookies = 1

参考: PHP: セッション処理関数(session) - Manual の「セッションとセキュリティ」

(3) サーバ側に存在しないセッションIDを受け付けてしまう

(2) の後半箇所の話。 session.use_only_cookies を 1 に設定することで、 URL に含まれるセッションIDは受け付けなくなる。 でも、セッション Cookie については状況は変わっていなくて、存在しないセッションIDを受け付けちゃう。

URL と違って Cookie の場合は他の利用者へ勝手に送りつけるのは難しそうだけど、いくつかの危険性はあるみたい。

どうも php.ini では対処できないみたい。 おさかなラボで紹介されている例 (セッションデータに特定の値が入っていないと、強制的にセッションIDを再発行する) を使えばよさそう。以下引用。

session_start();
if (!isset($_SESSION['initiated'])) {
    session_regenerate_id();
    $_SESSION['initiated'] = true;
}

ただ、このセッションIDを再発行する関数 (session_regenerate_id) が古いセッションファイルを削除しない仕様となっている (最新のPHP5ではオプションで回避できる) ようで、ゴミセッションが増えていくのが欠点。

参考:

(4) セッションの新規発行と読み込みが区別されていない

(ここの話はセキュリティよりも負荷問題かも)

PHP でセッションを読み込むときには、 session_start() 関数を呼ぶ。

  • 利用者からセッション Cookie が送られている時は、そのセッションIDに対応したセッションデータを読み込む
  • セッション Cookie が送られてきていない時は、新規にセッションIDを発行する。

常に全ページでセッション Cookie が必要な場合はこの方式でいいんだけど、ログイン後のみにセッション Cookie を発行したい場合には少し面倒なことになる。 (3) の対策を使ってセッションデータに特定の値が含まれるかどうかをチェックし、特定の値が含まれていなければ(利用者が正当なセッションCookieを持っていなければ)セッションを発行しない方法が良さそう。

session_start();
if (!isset($_SESSION['initiated'])) {
    session_destroy();
}

が呼ばれた時点で新規セッションが作られているので、 session_destroy() を使ってセッションを削除している。

このあと新規にセッションを発行する場合は session_start() と session_regenerate_id() 関数を呼ぶことになるんだけど、単にsession_regenerate_idしただけでは、古いセッションIDにリンクしたセッションデータが消えないという問題にぶつかる。

※ 蛇足だけど Ruby の場合はオプション値 (new_session => 'false') を使って新規セッションを作らないようにできる。

なお、この対策云々に関係なく、ログインなどの重要な操作の後には、セッションIDを再発行したほうがいいみたい。

参考: 「CSRF」と「Session Fixation」の諸問題について (高木さんのページの 2006.4.4 より)

他に気をつけたい点

セッション Cookie のパスのデフォルトがルート (/) になっていることかな。 同一サーバの他アプリとセッションが競合しちゃう。

いろいろあるなぁ…。 secure_session_start() みたいな関数が欲しいところ。

試しに作ってみたけど

まだまだ問題が多々あるよ…。

<?php
 /**
  * セッションIDを安全に発行する
  *
  * 攻撃者が指定したセッションIDを使わせない
  * ref. http://phpsec.org/projects/guide/4.html#4.1
  */
 function secure_session_start() {
   session_start();
   if (!isset($_SESSION['initiated'])) {
     // session_destroy();
     session_regenerate_id(TRUE);
     $_SESSION['initiated'] = true;
   }
 }

 /**
  * セッションが存在する場合はセッションデータを読み込む
  *
  * セッションCookieを持たないユーザはセッションを開始しない
  * [bug] session_read() 後に session_create() すると
  *       session_destroy() で消したはずの
  *       不要なセッションデータが消えない
  * ref. http://tdiary.ishinao.net/20050526.html#p02
  */
 function session_read() {
   if (!isset($_SESSION) && isset($_COOKIE[session_name()])) {
     // secure_session_start();
     session_start();
     if (!isset($_SESSION['initiated'])) {
       session_destroy();
       unset($_SESSION);
     }
   }
 }

 /**
  * 新しいセッションを生成する
  *
  * 既にセッションが存在する場合: セッションIDを変更
  *  セッションが存在しない場合: 新規にセッションを発行
  */
 function session_create() {
   // session_read();
   if (isset($_SESSION) && $_SESSION['initiated']) {
     session_regenerate_id(TRUE);
   } else {
     secure_session_start();
   }
 }

 session_read();
 $old_sessionid = session_id();
 if ($_GET['cmd'] == 'start') {
   session_create();
 }
 $new_sessionid = session_id();
?>
<html>
<body>
<?php
  echo "古いセッション: $old_sessionid<br />";
  echo "新しいセッション: $new_sessionid<br />";
?>
</body>
</html>

関連する日記