別の制作会社が作成したWebサイトを保守しているのだが、このほどセッションの処理が原因でちょっとしたトラブルが起きたので、クッキーとセッションについて改めて復習をしてみた。

クッキーとはなんぞ

WebサーバーからWebブラウザへHTTPレスポンスヘッダを利用して情報を送るためファイルのこと。
中身はキー/バーリューの形式になっている。
クッキーはサーバーにリクエストするたびに自動送信される。
1つのドメインに対して保存できるクッキーの数は20個までで、キー/バーリューをあわせて4KBまでが上限となる。
ちなみにクッキーとカタカナで書くとにわか臭がすごいが、今回はあえてそうしている。
Cookie

セッションとはなんぞ

サーバーが接続ごとに識別子(セッションID)を割り当て、セッションIDに紐づける形で、データを(セッション変数に)所有するのがWebアプリケーションにおけるセッションの概念。
セッションのデータはサーバーにファイルとして保存されている(場所はPHPだとphp.iniの「session.save_path」あるいは「session_save_path()」関数で指定されるば場所)。

HTTPはステートレスなプロトコルのため、状態を管理することはできないが、セッションによって固有の値を発行し、それをクッキーに保存する(PHPのsession_start関数は自動でクッキーに保存する)ことで誰がアクセスしているかというのをサーバー側で知ることができる。
他の方法としてはGETパラメーターに付与することなどがあるが、セッション固定攻撃の対象になりやすく、またRefererヘッダを経由してセッションIDが外部に漏洩する原因となるなど、あまり推奨されない。


パーフェクトPHPより引用。すごくわかりやすい図。

まとめるとサーバーがセッションIDを発行し、ブラウザのクッキーに保存させる。
そしてクッキーは必ずリクエストに含まれるため、誰が接続しているかを判断できる。
サーバーはセッションごとに値を保持できるので、そのユーザーが今までどのようなことをしてきたかの記録をとることができるというわけだ。
つまりクッキーとセッションは一心同体。いや、セッションのみがクッキーがなくては生きていけないため、単一方向の依存となるといったほうが正しい。

あえてGET、POSTと比較するならば、ページ遷移を適切に保つことができれば、GET、POSTでも同じような挙動は実現できるが、GET、POSTはクライアントサイドからデータを送信する必要があるところが異なる(なのでセッションが自動で行ってくれていたユーザーの認識を毎回ブラウザ側で操作する必要があり面倒)。

PHPでのセッションの発行方法

(1)PHPにおけるセッションの発行方法は、php.iniの「session.entropy_file」のファイルに記載されている文字列と、そのファイルからどれくらいの文字を抽出して読み込むか(session.entropy_length)を指定する。
「session.entropy_file」は「/dev/urandom」というランダム生成ファイルを指定するのが一般的。
「session.entropy_length」はハッシュ関数によって変動するが大きすぎると処理速度が長くなり、小さすぎるとランダム性が低くなる。

(2)そして、「session.hash_function」でハッシュ関数を指定(使用できるハッシュ関数はhash_algos()関数で確認できる)する。
一般的にはsha1が選ばれる(sha1の場合はブロック長が64バイトなので、session.entropy_lengthは64に指定するのが良い)。

(3)ハッシュ関数からの出力はバイト列なので、文字列に変換するために「
session.hash_bits_per_character」で何ビットごとに1文字にするかを指定する。
4(40字) or 5(32字) or 6(27字)のなかから選択するが、文字列長が長くなったからと言って強度が高くなるわけではなく、文字の種類が減るので組み合わせの数は変わらない。

(4)(1)と「接続元のIPアドレス」「現在の時刻」「線形合同法による擬似乱数」の4つ要素を(2)の関数を使用して掛け合わせることによってセッションIDが生成される。
つまりセッションの強度は文字列長を変えても文字の種類が変更するだけなので意味がないので、ハッシュ関数とエントロピーファイルとその文字範囲の指定でランダム性をあげることが強度強化につながる。

(5)クッキーはキー/バリューなので、セッションIDはバリューにして、キーを考える必要があるが、これはsession_name()関数で指定し、session_start()関数でセッションを開始する。
またセッションを保ったまま、セッションIDをアクセスごとに変更するような実装もあり、これはsession_start()関数のあとに、session_regenerate_id()関数を使用することでセッションIDの変更が可能となる。
※この場合、すぐに前のセッションIDを無効化すると、非同期で取得するようなデータがある場合にアクセスできなくなる(クッキーは画像などのデータにも送信されるため)から一定期間は旧セッションIDでも受け付けるように実装する

このような流れでセッションが実現されているが、セッションIDの発行において一意性を完全に保証することはできない。
単一ホストの場合でも限りなく低い可能性だが存在するが、重複チェックを行うことで回避できる。
複数ホストの場合はアクセスが集中すると発生する可能性が高くなる(それでも限りなく低い)が、セッションIDの接頭語を各ホストごとに固定化し、重複チェックをすることが回避できる。
また重要な処理を行う際にパスワード入力による多重チェックを行うなどすることで対策することも可能である。
他に考えられるのは、ログイン時にランダム文字列(トークン)を発行して、クッキーとセッションの両方に保存し、同一である場合のみ認証しているとみなす方法もある。

クッキーのセキュリティ確保術

クッキーはリクエストヘッダに含まれたり、JavaScriptから操作できたりなど、充分にセキュリティ面で気を配らなければいけない。

・secure属性
HTTPS通信時にのみクッキーを利用可能とする。

・httponly属性
JavaScriptから読み書きさせない。

・セッションIDと同時に最終ログイン日時やユーザーエージェントなどの情報を管理
異常値であれば即座に処理を終了するような実装をすると、セッションハイジャックを受けづらくなる。
またセッションとクッキーの両方にそれらの値を保存しておき、セッション開始直後に整合性をチェックするなどの方法もある。