セッションの有効期間とか設定とか挙動とかを調べました

PHPでログインページを作ったりするときに、よくセッションを使ったりすると思いますが、 じゃあセッションってどのようになってるのでしょうか。

[参考]セッション固定攻撃
[参考]GPC(GET/POST/cookie)以外の情報を送るアラワザ
[参考]アンダーバーのあるドメインではセッションクッキーは使用できません

セッションの破棄されるタイミング

ガベージコレクト(ガベージコレクション、ガーベッジコレクション、ガーベッジコレクタともいわれます)とは、『ごみ拾い』という意味です。

session_start()が行われたときに、session.gc_probabilityを分子、session.gc_divisorを分母とする確率で、 session.gc_maxlifetimeよりファイル更新日付の古いファイルをsession.save_pathから削除します。

デフォルトでは、1/100の確率で、24分より古いセッションファイルが消えます。
1/100の確率の実際の挙動はコチラ

しかしセッションファイルが消された場合でも$_SESSIONの値は取得できます。

Linuxサーバーでは、それ以外にもcronによる処理があるので、 /tmpは10日、/var/tmpは30日で消えます。
[参考]/tmpや/var/tmpのファイル消えるタイミング

セッションの有効期限を長くする、短くする

セッションの長さを変更するには、session.gc_maxlifetimeを変えればよいと思うかもしれませんが、 時間を長くする場合には、これ以外にも設定が必要です。

同じサーバーでsession.gc_maxlifetimeが短いサイトがあった場合、ドメインが違ったとしても、そのタイミングでセッションは削除されます。
他のサイトやページに影響を受けないようにするには、セッションファイルの保存先session.save_pathを変える必要があります。
セッションはドメインの区別をしていません。

session.save_path = /tmp/example.com

とかです。
ただ/tmp以下は、10日で、またはサーバーの再起動時に削除されてしまうのでそれだと困る場合には

session.save_path = /var/tmp/example.com

とかにします。/var/tmp以下は30日で削除されます。

ただしsave_pathの設定はSAFE MODEが有効になっているときには変更できません。

セッションの有効期限を厳密に決める

セッションが破棄されるまでの時間は、session.gc_maxlifetimeとなりますが、厳密にはそうでありません。
デフォルトでは1/100の確率で、この時間です。

ではこの時間を厳密にするには、ガベージコレクトを毎回行わせる(確率を1にする)ように以下の設定するという方法があります。

session.gc_probability = 1
session.gc_divisor = 1

しかしこの設定はちょっとおかしなところがあります。
そもそもこの確率を設定する理由というのが、『毎アクセスごとにセッションの古いファイルがあるかどうか確認するのは負荷がかかるので』ということです。
そうであれば正しい手法は、セッションのデータにアクセスした時間を記録しておいて、この時間と比較するという方法でしょう。
もちろんこの場合でもsession.gc_maxlifetimeは設定する必要があります。

またセッションの動作を検証した結果は以下のようになっています。

(1)セッションを開始する。
(2)セッションの情報を読む。
(3)ガベージコレクトをする。
(4)セッションファイルを作成したり、ファイル日付を更新したりする。
(5)セッションの情報に変化があれば、その都度データを書き込む。
(6)セッションを閉じる。

(3)と(4)の順番は検証ができないので、確かではありません。

この順番になっているため、セッションが破棄される場合でもデータは読まれています。
session.gc_maxlifetimeだけでは正確に時間を決めることはできないのです。
実験結果

セッションの設定

php.iniで設定するなら、設定値を変えるだけです。

httpd.confや.htaccessに書くなら次のようにします。

php_value session.gc_maxlifetime 1800
php_value session.gc_probability 1
php_value session.gc_divisor 100


またPHPファイルに書くなら

session_save_path("/tmp");
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 100);

のように書くのですが、この場合は注意が必要で、必ずsession_start()より前に書く必要があります。

session_start()の際にはキャッシュなどのヘッダー情報が送信されるため、これより前にはブラウザへの出力処理はしてはいけません。

セッションファイルの状態

セッションファイルのファイル名は、sess_【セッションID】のようになっています。

例:sess_4cbdfb3df1bb2f40fb0faa4b17d20966

またファイルの内容は、【セッション名】|s:【セッションの中のID】:【値】のようになります。

数値型なら
$_SESSION["test"]=12345;は

test|i:1:12345;

文字列型なら
$_SESSION["test"]="hoge";は

test|s:1:"hoge";

値が複数なら
$_SESSION["test"]="hoge";
$_SESSION["test2"]="example";は

test|s:1:"hoge";test2|s:2:"example";

書き込む順番によっては

test2|s:1:"example";test|s:3:"hoge";

とか

test2|s:1:"example";test|s:3:"hoge";test|s:1:"example";

値に『"』があったら
$_SESSION["test"]='"';

test|s:1:""";

ここからも分かるとおり、セッションファイルにはセッションIDとセッションデータ以外のものは保持されていません。つまりはドメインの区別はしていません。

セキュアなセッションの使い方

ログイン処理

クッキーでセッションIDが送られてきてるかもしれないので、 ログインなどでセッションに値を入れ始める前にセッションIDを変更する。

session_regenerate_id(true);

引数にtrueを入れると、既に入っている値も削除されます。

ログアウト処理

session_destroy();としても、それ以降でセッション変数が残っているのでこの値をリセットする。

session_unset();

または

$_SESSION = array();

このセッションIDがクッキーに残るのでクッキーを消す。

setcookie(session_name(), '', time() - 3600, "/");

その他

SSLのページから非SSLのページへは、同じサーバーであれば同じセッションIDで値を引き継ぐことはできますが、絶対にやってはいけません。
どうしてもやらざる得ないときには、都度セッションIDを変更したほうが安全です。

ユーザーページと管理ページが同じドメインのとき

session_name('admin');

上記のようにして、どちらかのセッション名を変更すると別々のセッションIDが発行されるため片方でログアウトしても、もう片方はログアウトされません。

ガベージコレクトの1/100の確率って

1/100の確率というのは実際には、分母にあたるアクセスのカウンターがあって分子の数字より小さかったら実行するって動作ではないです。
0〜1の乱数を生成して、この値が設定されているGCの確率値(1/100)より小さかったらGCを実行するという挙動です。

いろいろな実験

SAFE MODEが無効な状態で、/tmp内も全て削除しておきます。 実行したコードは以下です。

<?
//セッションの保存先を明確にします
session_save_path("/tmp");

//実験のためセッションが消される時間を10秒と短くします
ini_set('session.gc_maxlifetime', 10);

//ガベージコレクトを毎回行うようにします
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1);

//session_start();の前には、出力ができないので後で出力できるようにします
$test="";

//session_start();の前のセッション保存ディレクトリを見ます
$dir1 = scandir(session_save_path());

//session_start();の前にセッションが読めるか見ます
if(isset($_SESSION["test"]))
    $test.= "session_start()の前<br />";

//ここからセッションを始めます
session_start();

//セッションが読めるか、またセッションの内容を見ます
if(isset($_SESSION["test"]))
    $test.= "session_start()の後".$_SESSION["test"]."<br />";

//セッションIDを見ます
$test.= session_encode()."<br />";

//セッション保存ディレクトリを見ます
$dir2 = scandir(session_save_path());

//セッションに何か値を入れます
$_SESSION["test"]=time();

//セッションが読めるか、またセッションの内容を見ます
if(isset($_SESSION["test"]))
    $test.= "値の設定後".$_SESSION["test"]."<br />";

//セッションIDを見ます
$test.= session_encode()."<br />";

//セッション保存ディレクトリを見ます
$dir3 = scandir(session_save_path());

//結果を出力します
echo "<pre>";
print_r($dir1);
print_r($dir2);
print_r($dir3);
echo "</pre>";
echo $test;
echo session_id();
?>
<a href="session.php?<?=SID?>">リロード</a>

これを実行すると、次のようになりました。

Array
(
    [0] => .
    [1] => ..
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_4cbdfb3df1bb2f40fb0faa4b17d20966
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_4cbdfb3df1bb2f40fb0faa4b17d20966
)

値の設定後1232485014
test|i:1232485014;
4cbdfb3df1bb2f40fb0faa4b17d20966

セッションファイルが作成されるタイミングは、session_start()のときのようです。
値の設定後にすぐにファイルに書き込まれるようです。

すぐにテキストリンクをクリックすると

Array
(
    [0] => .
    [1] => ..
    [2] => sess_4cbdfb3df1bb2f40fb0faa4b17d20966
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_4cbdfb3df1bb2f40fb0faa4b17d20966
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_4cbdfb3df1bb2f40fb0faa4b17d20966
)

session_start()の後1232485014
test|i:1232485014;
値の設定後1232485022
test|i:1232485022;
4cbdfb3df1bb2f40fb0faa4b17d20966

session_start()の前には値は取れません。
値の設定後にすぐにファイルは変更されるようです。

しばらくしてテキストリンクをクリックすると

Array
(
    [0] => .
    [1] => ..
    [2] => sess_4cbdfb3df1bb2f40fb0faa4b17d20966
)
Array
(
    [0] => .
    [1] => ..
)
Array
(
    [0] => .
    [1] => ..
)

session_start()の後1232485022
test|i:1232485022;
値の設定後1232485182
test|i:1232485182;
4cbdfb3df1bb2f40fb0faa4b17d20966

session_start()のときにガベージコレクトがされているようです。
ファイルが無くても、値は取れているので、ガベージコレクトの前に値の取得がされているようです。
ファイルが無くてもsession_encode()の値が変わっているので、これは実際のファイル内容ではないようです。
となると、セッションファイルへの変更情報はsession_encode()に保持され、最後にセッションファイルに書き込まれる、というのが正しいようです。

今度は、session_destroy();を入れてセッションを破棄するように、少しファイル内容を変えてみます。

<?
session_save_path("/tmp");
ini_set('session.gc_maxlifetime', 1000);
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1);

$test="";
$dir1 = scandir(session_save_path());

session_start();

if(isset($_SESSION["test"]))
    $test.= "session_start()の後".$_SESSION["test"]."<br />";
$test.= session_encode()."<br />";

$dir2 = scandir(session_save_path());

session_destroy();

$dir3 = scandir(session_save_path());

if(isset($_SESSION["test"]))
    $test.= "session_destroy();の後".$_SESSION["test"]."<br />";
$test.= session_encode()."<br />";

echo "<pre>";
print_r($dir1);
print_r($dir2);
print_r($dir3);
echo "</pre>";
echo $test;
echo session_id();
?>
<a href="session.php?<?=SID?>">リロード</a>

これを読み込むと次のようになりました。

Warning: session_encode() [function.session-encode]: Cannot encode non-existent session.

Array
(
    [0] => .
    [1] => ..
    [2] => sess_d7d6bd4d39d96a8bbbe9b8f2f3530534
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_d7d6bd4d39d96a8bbbe9b8f2f3530534
)
Array
(
    [0] => .
    [1] => ..
)

session_start()の後1232494639
test|i:1232494639;
session_destroy();の後1232494639

セッションがないと、session_encode()はエラーが出ます。
session_destroy();の後で、セッションファイルは消えています。
session_destroy();としたとしても、セッションのデータは取得できています。

さらにテキストリンクをクリックすると次のようになります。

Warning: session_encode() [function.session-encode]: Cannot encode non-existent session.

Array
(
    [0] => .
    [1] => ..
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_d7d6bd4d39d96a8bbbe9b8f2f3530534
)
Array
(
    [0] => .
    [1] => ..
)

セッションIDは変わらないようです。

ヘッダー情報を確認すると、ブラウザからPHPSESSID=d7d6bd4d39d96a8bbbe9b8f2f3530534のクッキーを送ってます。
また少しファイルを変えてみます。

<?
session_save_path("/tmp");
ini_set('session.gc_maxlifetime', 1000);
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1);

$test="";
$dir1 = scandir(session_save_path());

session_start();

$dir2 = scandir(session_save_path());

session_destroy();
setcookie(session_name(), '', time() - 3600);

echo "<pre>";
print_r($dir1);
print_r($dir2);
echo "</pre>";
?>
<a href="session.php?<?=SID?>">リロード</a>

これを2回読み込むと次のようになりました。

Array
(
    [0] => .
    [1] => ..
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_d7d6bd4d39d96a8bbbe9b8f2f3530534
)

まだセッションIDは変わらないようです。

ヘッダー情報を確認すると、ブラウザからPHPSESSID=d7d6bd4d39d96a8bbbe9b8f2f3530534のクッキーを送ってます。
サーバーからはSet-Cookie: PHPSESSID=deleted; expires=Tue, 22-Jan-2008 01:25:16 GMTと、 セッションを消すような情報はちゃんと送ってます。

また少しファイルを変えてみます。

<?
session_save_path("/tmp");
ini_set('session.gc_maxlifetime', 1000);
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1);

$dir1 = scandir(session_save_path());

session_start();

$dir2 = scandir(session_save_path());

session_destroy();
setcookie(session_name(), '', time() - 3600, "/");

echo "<pre>";
print_r($dir1);
print_r($dir2);
echo "</pre>";
?>
<a href="session.php?<?=SID?>">リロード</a>

これを2回読み込むと次のようになりました。

Array
(
    [0] => .
    [1] => ..
)
Array
(
    [0] => .
    [1] => ..
    [2] => sess_50d75d5dc45913ab63d9bd5120959fce
)

無事、セッションが変わりました。
セッションクッキーを削除するときには、パスまで指定しないと消えないということです。
クッキーについての実験
ちなみに/tmpに手動で『 sess_ 』というファイルを置くと、ガベージコレクトのタイミングで削除されます。
『 sess 』というファイルは削除されません。

http://www.example.php?PHPSESSID=test
と適当なセッションIDをGETクエリで付けてアクセスすると、セッションファイルが無くても『 sess_test 』というセッションファイルが作成され、 このセッションIDは有効になります。

http://www.example.php?PHPSESSID=test
と適当なセッションIDをGETクエリで付けて、さらにクッキーでPHPSESSIDを『 test2 』としてアクセスすると、 クッキーのほうが優先されて『 sess_test2 』というセッションファイルが作成されます。

http://www.example.php?PHPSESSID=あいうえお
としてみると、
Warning: session_start() [function.session-start]: The session id contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,'
セッションIDに使用できるのは、『 aからz、AからZ、0から9、および「-(ハイフン)」』で構成されたものじゃないと駄目です、と怒られます。

http://www.example.php?PHPSESSID=----
としてみると、
『 sess_---- 』というセッションファイルが作成されます。

クッキーがない状態で、
http://www.example.com/test.php
とセッションを発行するページにアクセスすると、クッキーを書く命令がサーバーからは返されます。
また<?=SID>の部分には『 PHPSESSID=【セッションID】 』と出力されます。

クッキーがない状態で、
http://www.example.com/test.php?PHPSESSID=test
と適当なセッションIDをGETクエリで付けてアクセスすると、クッキーを書き換える命令はサーバーからは返されません。
また<?=SID>の部分には『 PHPSESSID=test 』と出力されます。
これはセッション固定攻撃(session fixation)の対象となります。
あるサイトに対して、http://example.com?PHPSESSID=testのようなリンクを張ってしまい、セッションハイジャックを行う攻撃です。

session_regenerate_id();が書かれたページに、
クッキーがない状態で、
http://www.example.com/test.php?PHPSESSID=test
と適当なセッションIDをGETクエリで付けてアクセスすると、新しいセッションIDにクッキーを書き換える命令がサーバーからは返されます。

セッションIDがクッキーにある状態でアクセスすると、クッキーを書く命令はサーバーからは返されません。
また<?=SID>の部分には何も出力されません。
つまり
http://www.example.com/test.php?<?=SID>&a=test
とすると
http://www.example.com/test.php?&a=test
となってしまうため、<?=SID>はGETクエリの最後に書かないといけません。

session_regenerate_id();が書かれたページに、
クッキーがない状態で、
http://www.example.com/test.php?PHPSESSID=test
と適当なセッションIDをGETクエリで付けてアクセスすると、新しいセッションIDにクッキーを書き換える命令がサーバーからは返されます。

ini_set("session.cookie_path","/test/");と書かれたページに、
クッキーがない状態でアクセスすると、『 /test/ 』ではなくて『 /test 』のパスでクッキーを書く命令がサーバーからは返されます。

session_name("hoge&<test>あ");
とすると、 <?=SID>の部分には『 hoge&<test>あ=【セッションID】 』とHTMLエスケープはされず出力されます。
変数名『 hoge&<test>あ 』とクッキーを書く命令がサーバーからは返されます。
session_nameには命名規則はないみたいですね。
print_r($_COOKIE);
として確認すると次のアクセスでInternet ExplorerもFire Foxも、変数名『 hoge&<test>あ 』というクッキーを送っています。

cookie、POST、GETにセッションIDを付けないでセッションを使用する

セッションID以外にユーザーの一意なIDがとれるなら、GETなどにセッションIDを付与する必要はありません。
例えば一意なIDを$uidとするとき、session_start()の前にsession_id($uid)とすることで、内部的にセッションが使用できます。
ただしセッションIDには『 aからz、AからZ、0から9、および「-(ハイフン)」』しか使えません。
一意なIDが推測可能なものであれば偽装されないように気をつけましょう。

携帯電話のモバイルサイトで個体識別情報が取得できるとき

アクセスの多いサイトでセッションを使う場合

ユニークユーザーの多いサイトでは、それだけセッションファイルが多くなる。このときガベージコレクトにかかる時間はディスクI/Oに依存し、 たまたまガベージコレクトに出くわしたユーザーは、ページが表示されるまでの時間が長くなる。
このようなときには、ガベージコレクトを使わず(session.gc_probability = 0にする)、cronでセッションファイルを削除すると良い。

以下は10分おきに1時間より古いセッションファイルを削除するクーロンの例

*/10 * * * * daemon nice -n 18 find /tmp -type f -name "sess_*" -mmin +60 -exec rm

セッションのデフォルトの設定

session.save_handler = files

セッションをファイルに保存する。

session.save_path = /tmp

保存するファイルの場所は『/tmp』とする。

session.use_cookies = 1

クッキーが使えるなら使う。

session.name = PHPSESSID

セッションの変数名は『PHPSESSID』とする。

session.auto_start = 0

セッションを自動的に開始しない。

session.cookie_lifetime = 0

クッキーを使用する場合、有効期間はブラウザを閉じるまでとする。

session.cookie_path = /

クッキーを使用する場合、有効なURIは『/』(全ての階層)とする。

session.cookie_domain =

クッキーを使用する場合、ドメインは限定しない。(別々のドメインで持ち回りができるということではない)

session.cookie_secure =

クッキーを使用する場合、SSL接続のみとしない。

session.gc_probability = 1

ガベージコレクトの確率の分子は『1』とする。

session.gc_divisor = 100

ガベージコレクトの確率の分母は『100』とする。
つまりは1/100=1%の確率でガベージコレクトを行う。

session.gc_maxlifetime = 1440

ガベージコレクトにより破棄されるファイルの古さ(セッションが有効な時間)は1440秒=24分とする。

スポンサーリンク

関連記事

スポンサーリンク

LPAD関数 文字を充填する

ホームページ製作・web系アプリ系の製作案件募集中です。

上に戻る