[PHP] リクエストパラメータ・セッションに関するまとめ

  • 352
    いいね
  • 1
    コメント

予備知識

スーパーグローバル変数とは?

「スーパーグローバル変数って何?」って感じの駆け出しPHPプログラマのために念のためマニュアルへのリンクを記載しておきます.全然知らない人は軽く読んでおいてください.

HTTPとは?

リクエストヘッダー・レスポンスヘッダー と聞いてピンと来ない人はまず下記サイトにて予習をお願いします.細かいことは覚える必要は無いので,大雑把に「ヘッダーとはどんなものか」ということを理解してください.

スーパーグローバル変数の生成

リクエストが来たとき,それに応じた $_GET $_POST $_COOKIE が生成される様子を見ていきましょう.

連想配列のパラメータを文字列で表現する

まず,このような連想配列のデータがあったとき…

$data = [
    'A' => 'B',
    'C' => [
        'D' => 'E',
    ],
];

それを文字列として表現するには,大雑把に言うと2種類の形式があることを覚えてください.それぞれ, クエリ―ストリング および Cookieヘッダー といいます.

クエリ―ストリングの場合
A=B&C[D]=E
Cookieヘッダーの場合
Cookie: A=B; C[D]=E

なんとなくわかりますかね?

  • キーAの値はB
  • キーCに対応する値は配列で,さらにそのキーDに対応する値がE

基本はこれだけで十分です.PHPはこれらの文字列を解析し,もとの配列に復元する仕事をしています.細かなパース規則については付録にしておきましたので,厳密にいろいろ試したい人は読んでください.

PHPがWebブラウザからパラメータを受け取る方法 (基本編)

以下の2点に留意してお読みください.

  • ヘッダーの1行1行を区切るために使用される改行コードは CRLF "\r\n" である.
  • ヘッダーの生成と送信はWebブラウザが全自動でやってくれるので,普段私たちが普通にWebブラウザを使う限りは意識することはない.

URLにクエリ―ストリングを含める

クエリ―ストリングはURLの ? より後ろの部分に記述されます.

送信側

リンクをクリックするとリクエストが実行される
<a href="http://example.com/test.php?a=b">Test</a>
フォームを送信するとリクエストが実行される
<form method="get" action="http://example.com/test.php">
<input type="hidden" name="a" value="b">
<input type="submit" value="送信">
</form>
リクエストヘッダー
GET /test.php?a=b HTTP/1.1
Host: example.com
Connection: Close

受信側

クエリ―ストリングは $_SERVER['QUERY_STRING'] に格納されます.

文字列としてこのようにセットされる,もしくは未定義
$_SERVER['QUERY_STRING'] = 'a=b';

私のテストした環境では ? がURL中に存在しない場合でもこのインデックスは 空文字列 として常に存在するようですが,マニュアルでは未定義になる可能性が示唆されているため,使用する場合は以下のようにしてください.

  • isset でチェックする
  • ??演算子でチェックする (PHP7.0以降)
  • filter_input 関数経由で取得する
これらのうち,いずれかの記述を採用する
$query_string = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
$query_string = $_SERVER['QUERY_STRING'] ?? '';
$query_string = (string)filter_input(INPUT_SERVER, 'QUERY_STRING');

これをパースして配列に格納したものが $_GET です.この変数は常に存在しています.

配列としてこのようにセットされる
$_GET = ['a' => 'b'];

ポストフィールドにクエリ―ストリングを含める

送信側

クエリ―ストリングはヘッダー群の一番下に空行を1つ挟んで配置され,ここは ポストフィールド と呼ばれることがあります.以下のヘッダーを必ず伴います.

Content-Length

クエリ―ストリングの バイト数 を表します.

Content-Type

application/x-www-form-urlencoded 固定です.

フォームを送信するとリクエストが実行される
<form method="post" action="http://example.com/test.php">
<input type="hidden" name="a" value="b">
<input type="submit" value="送信">
</form>
リクエストヘッダー
POST /test.php HTTP/1.1
Host: example.com
Content-Length: 3
Content-Type: application/x-www-form-urlencoded
Connection: Close

a=b

受信側

クエリ―ストリング部分が文字列で欲しい場合, php://input をファイルのように取得させます.

文字列で取得できる
$postfields = file_get_contents('php://input');

これをパースして配列に格納したものが $_POST です.この変数は常に存在しています.

配列としてこのようにセットされる
$_POST = ['a' => 'b'];

Cookieヘッダーを送信する

Cookieを保存させる

setcookie 関数または setrawcookie 関数を使うと,ブラウザに対して簡単に整形された Set-Cookieヘッダー を送信することが出来ます.前者は値のみパーセントエンコードし,後者はキー・値ともそのままセットします.

PHPでこれを実行する
setcookie('a', 'b');
Webブラウザに送信されるSet-Cookieヘッダー
(基本形であって,実際にはこれの後ろにいろいろな情報が付加される)
Set-Cookie: a=b

これをブラウザが受理すると,次回から同一サイトに対してCookieヘッダーを送信してくれるようになります.この関数を実行したタイミングではCookieヘッダーは送信されてきていないことに注意してください.また,これらの関数で以下のパラメータを設定すると,Cookieの挙動を詳細に制御することが出来ます.

$expire

有効期限.タイムスタンプとして表される.

  • 省略または 0 を指定した場合,Webブラウザを閉じるまでが有効期限となる.
  • 0 より大きく現在より小さい値(過去)を指定した場合,即座にWebブラウザで削除が行われる.但し, 共通な $path $domain を指定していなければならない.
よく見かける例
setcookie('foo', '', time() - 3600);
実はこれでもOK
setcookie('foo', '', 1);
$path

どこの階層から送信を行うかを表したパス.省略した場合は スクリプトが存在しているディレクトリ を指定したと見なされる.

$domain

対象となるドメイン名.省略した場合はスクリプトが存在するサーバーのものがそのまま使われる.

$secure

HTTPS接続のみに限定するかどうか.デフォルト値は false

$httponly

JavaScriptからのアクセスをブロックするかどうか.デフォルト値は false

これらを指定したとき,送出されるヘッダーは以下のようになります.

(実行例) PHPコード
setcookie('a', 'b', 992174400, '/foo/bar', 'example.com', false, true);
(実行結果) Set-Cookieヘッダー
Set-Cookie: a=b; expires=Sun, 10-Jun-2001 12:00:00 GMT; path=/foo/bar; domain=example.com; httponly

Cookieを受信する

文字列のままのCookieは頭のCookie:を除いて $_SERVER['HTTP_COOKIE'] に格納されます.

文字列としてこのようにセットされる,もしくは未定義
$_SERVER['HTTP_COOKIE'] = 'a=b';

Cookieヘッダーが無い場合,このインデックスは存在しません.使用する場合は $_SERVER['QUERY_STRING'] と同様のチェックが必要です.

これらのうち,いずれかの記述を採用する
$cookie = isset($_SERVER['HTTP_COOKIE']) ? $_SERVER['HTTP_COOKIE'] : '';
$cookie = $_SERVER['HTTP_COOKIE'] ?? '';
$cookie = (string)filter_input(INPUT_SERVER, 'HTTP_COOKIE');

これをパースして配列に格納したものが $_COOKIE です.この変数は常に存在しています.

配列としてこのようにセットされる
$_COOKIE = ['a' => 'b'];

PHPがWebブラウザからパラメータを受け取る方法 (応用編)

URLにパス情報を含める

ファイルをディレクトリのように見せ,その後ろに追加で情報を階層的に記述することが可能です.

送信側

リンクをクリックするとリクエストが実行される
<a href="http://example.com/test.php/foo/bar">Test</a>
リクエストヘッダー
GET /test.php/foo/bar HTTP/1.1
Host: example.com
Connection: Close

受信側

パス情報は $_SERVER['PATH_INFO'] に格納されます.

文字列としてこのようにセットされる,もしくは未定義
$_SERVER['PATH_INFO'] = '/foo/bar';

この部分が無い場合,このインデックスは存在しません.使用する場合は $_SERVER['QUERY_STRING'] と同様のチェックが必要です (省略) .

【注意】
+ は半角スペースに置換されませんrawurldecode 関数相当の処理です.

ポストフィールドにマルチパートコンテンツを含める

パラメータがパーセントエンコードされないのが特徴です.そのため,通信量が肥大化しやすいファイルのアップロードに利用されます.

送信側

マルチパートコンテンツはヘッダー群の一番下に空行を1つ挟んで配置され,以下のヘッダーを必ず伴います.

Content-Length

マルチパートコンテンツの 総バイト数 を表します.

Content-Type

multipart/form-data; boundary=境界文字列 という形式です.改行などを除く任意の文字列を設定できますが,送信するデータと衝突する可能性の無いランダムなハッシュ値を採用することが多いと思います.ブラウザによって実装は異なるようです.

各コンテンツは以下のような形式です.

通常のパラメータ
--境界文字列
Content-Disposition: form-data; name="NAME属性値"

送信するVALUE属性値
--境界文字列
Content-Disposition: form-data; name="NAME属性値"; filename="ファイル名"
Content-Type: MIMEタイプ(ブラウザが自動判定)

送信するファイルデータ

これらが連続で配置され,最後のパラメータの後には次の1行が挿入されます.

終端行
--境界文字列--

以下にリクエストの例を示します.

フォームを送信するとリクエストが実行される
<form method="post" action="http://example.com/test.php" enctype="multipart/form-data">
<input type="hidden" name="hidden" value="あいうえお">
<input type="file" name="file">
<input type="submit" value="送信">
</form>
リクエストヘッダー
POST /test.php HTTP/1.1
Host: example.com
Content-Length: 257
Content-Type: multipart/form-data; boundary=-----hogehoge
Connection: Close

-------hogehoge
Content-Disposition: form-data; name="hidden"

あいうえお
-------hogehoge
Content-Disposition: form-data; name="file"; filename="sample.txt"
Content-Type: text/plain

サンプルファイルデータ
-------hogehoge--

受信側

ファイルに関するものは $_FILES ,それ以外のものは $_POST に格納されます.これ以上の説明はここでは割愛しますので,参考リンクだけ記載しておきます.

$_GET $_POST に同時に格納させる

method属性がpostのとき,action属性の値に ? に続いてクエリを付加することで実現できます.ただし,あまり出番はありません.というよりもややこしいので,使わないほうがいいです.

フォームを送信するとリクエストが実行される
<form method="post" action="http://example.com/test.php?a=b">
<input type="hidden" name="c" value="d">
<input type="submit" value="送信">
</form>
配列としてこのようにセットされる
$_GET = ['a' => 'b'];
$_POST = ['c' => 'd'];

$_GET $_POST $_COOKIE をまとめて扱う

$_GET$_POST$_COOKIE の順に上書き代入を行い,生成されるスーパーグローバル変数が $_REQUEST です.この変数は常に存在しています.これもややこしさを助長するだけですので,使わないほうがいいです.

PHPから他のスクリプトにリクエストを送信する方法

これまでに紹介した方法は全て Webブラウザ から送信されてきた情報を 自身のスクリプト で処理する方法についてでした.これとは異なり, 自身のスクリプト から 他のスクリプト に情報を送信することも可能です.PHPスクリプトにWebブラウザの代役をさせると考えれば分かりやすいでしょう.

よく使われる関数を紹介しておきます.

またこれらを用いて,Webページから必要な情報を抜き出す手法もあります.それはスクレイピングと呼ばれます.

セッション関連あれこれ

ここからは セッション を扱っていきます.前半で紹介した知識を前提としている箇所もあるので,忘れてしまったら戻って読み返してみてください.

セッションって何? なぜセッションが必要なの?

これまで紹介したスーパーグローバル変数 $_GET $_POST $_COOKIE $_REQUEST は,情報をユーザー側に送信させるものでした.これは,たとえ $_GET$_POST 用のパラメータがHTMLの中に埋め込まれているものである,また一目にはユーザーにその存在を感じさせない $_COOKIE であったとしても,閲覧なんて当たり前,更には改竄しようと思えばいくらでも改竄できるこということを示しています.ブラウザゲームで自分の所持金をCookieで管理していたら,

【A】文字列になるはずのところに配列をセットしてプログラムを誤作動させる
$_COOKIE['money'] = [];
【B】所持金MAX!!
$_COOKIE['money'] = '99999999';

これに相当することが出来てしまうのです.【A】に関しては

Qiita - $_GET, $_POSTなどを受け取る際の処理

ここで紹介しているような

$money = (int)filter_input(INPUT_COOKIE, 'money');

こういったフィルタリングを行うことでエラーの発生を回避させることは出来ますが,【B】に関しては本当にそれがあり得る範囲の値であればどうしようもないですよね.これがCookieだけで管理できる情報の限界です.

そこで,ブラウザ側ではなくサーバー側に情報を保存するというスタンスで生まれたのが セッション なのです.

基本的な使い方は?

session_start 関数により $_SESSION 変数を準備し,あとは値の取得/代入といった操作を実行すればいいだけです.

マニュアルより8割ぐらい引用
<?php

// セッションを開始
@session_start();

if (!isset($_SESSION['count'])) {
   // 初回にはcountを0に初期化
   $_SESSION['count'] = 0;
}
// インクリメント
++$_SESSION['count'];

header('Content-Type: text/html; charset=utf-8');

?>
<!DOCTYPE html>
<html>
<head>
  <title>あなた専用カウンター</title>
</head>
<body>
  <p>
    こんにちは,あなたがこのページに来たのは<?=$_SESSION['count']?>回目ですね.<br>
    F5で更新すると再読込します.
  </p>
</body>
</html>

session_start の前に @ をつけているのは,不正なリクエストが来ると E_NOTICE 更には E_WARNING レベルのエラーも発生し得るからです.PHP言語の欠陥ですね,これは….

(しかし,実際にWebサービスとして運用するときにはエラーは非表示になるので,必ずしもこれは必須ではありません.また,いきなりこれを付加してしまうとセッションが何らかの理由で開始出来なかったときに原因不明のバグに悩まされることもあるので,十分に注意してください.)

以降では,@をつけないことにします.

スーパーグローバル変数への代入ってタブーじゃないの?

以下のように変数の存在をチェックして初期化するコードはよく見かけますよね.

三項演算子を用いた例
$data = isset($_POST['data']) ? $_POST['data'] : '';
filter_input関数を活用して型チェックまで行う例
$data = (string)filter_input(INPUT_POST, 'data');

一方,こんな風に上書き代入しちゃうコードはあんまり見かけませんね.コーディング規約でも禁止している場合が多いと思います.

if (!isset($_POST['data'])) {
    $_POST['data'] = '';
}

ところが $_SESSION だけは例外で,直接操作することが広く認められている唯一のスーパーグローバル変数となります.

(但し, session_start 関数のエラーの発生を @ 演算子無しに防ぐために $_COOKIE を操作することは例外的に認められるでしょう.このコードは後ほど出てきます.)

セッションに格納できるキーは?

10進整数 と等価でない文字列であれば何でも格納できます.

GOOD
$_SESSION['nine'] // 通常の文字列
$_SESSION['010']  // 8進表記文字列
BAD
$_SESSION[8]   // 10進表記整数
$_SESSION[010] // 8進表記整数(実質的には上記と等しい)
$_SESSION['8'] // 10進表記文字列
$_SESSION['0'] // 10進表記文字列

セッションに格納できる値は?

$_GET $_POST $_COOKIE $_REQUEST などは 文字列配列 しか扱えませんでしたが,$_SESSION はリソース型以外の 任意のデータ を保存することが出来ます.但しオブジェクトを保存する場合,そのクラスの定義が session_start 関数のコールよりも前に行われている必要があります.

GOOD
require 'Test.class.php';
session_start();
BAD
session_start();
require 'Test.class.php';

Cookieとどう違うの?

既に述べたように

  • セッションの方が保存できるデータ型が多い
  • セッションはサーバー側に保存されるのでユーザーに干渉を受けない

などの違いはありますが, 「Cookieとどう違うの?」 という質問自体に少し語弊があります.というのも,セッションはCookieのもとに成り立っているからです.サーバー側にデータを保存したところで,それが誰のものかが分からないと意味がないですよね.これをCookieによって判別するわけですが,デフォルトでは 'PHPSESSID' という名前のCookieを使って判定することになっています.以下のような処理が為されます.

  1. ユーザーが初めてページにアクセスしてくる.
  2. session_start 関数が初めてコールされ,このときに
    Set-Cookie: PHPSESSID=d41d8cd98f00b204e9800998ecf8427e; path=/
    のように他と重複しないランダムな値が自動生成されてユーザー側に送られる.
  3. スクリプト内で $_SESSION に(必要があれば)代入などを行う.
  4. スクリプト終了時, セッションファイル sess_d41d8cd98f00b204e9800998ecf8427e に, $_SESSION の内容が復元可能な形で自動的に書き込まれる.

ここからは2回目のアクセスです.

  1. ユーザーが2回目にページにアクセスしてくる.
  2. session_start 関数がコールされると,まず最初に$_COOKIE['PHPSESSID']が存在しているかのチェックが行われ,それを元に該当する名前のセッションファイルが存在しているかどうかを調べる.
  3. 存在している場合に限り,セッションファイル sess_d41d8cd98f00b204e9800998ecf8427e からデータが復元され, $_SESSION が準備される.
  4. スクリプト内で $_SESSION に(必要があれば)代入などを行う.
  5. スクリプト終了時, セッションファイル sess_d41d8cd98f00b204e9800998ecf8427e$_SESSION の内容が復元可能な形で自動的に書き込まれる.

以降は繰り返しです.

2016/07/23追記
図を用いた分かりやすい記事があったので引用します

セッションファイルってどこにどんな名前で保存されてるの?

セッションを識別するための3つの概念を述べます.

  • セッション用Cookieの名前
  • セッション用Cookieの値
  • セッションファイルの保存パス

これらは,以下の何れかの方法で設定または取得することができます.

  • php.iniの編集
  • ini_set 関数または ini_get 関数
  • session_start 関数の第1引数 (PHP7.0以降のみで使えるオプション)
  • 専用の関数

セッション用Cookieの名前

これらのうちいずれかを使う
// 取得
$path = session_save_path();
$path = ini_get('session.save_path');

// 設定
session_save_path('/tmp');
ini_set('session.save_path', '/tmp');
session_start(['save_path' => '/tmp']); // PHP7.0以降
  • 取得は常に可能です.
  • 設定は session_start 関数をコールする でなければなりません.
  • デフォルト値は空であり,その場合 sys_get_temp_dir 関数の返り値で取れる,システム既定のテンポラリディレクトリが使われます.

セッション用Cookieの名前

これらのうちいずれかを使う
// 取得
$name = session_name();
$name = ini_get('session.name');

// 設定
session_name('MYSESSID');
ini_set('session.name', 'MYSESSID');
session_start(['name' => 'MYSESSID']); // PHP7.0以降
  • 取得は常に可能です.
  • 設定は session_start 関数をコールする でなければなりません.
  • デフォルト値があらかじめ 'PHPSESSID' として設定されています.

セッション用Cookieの値

// 取得
$id = session_id();

// 設定
session_id('vmtflhhks040679m9rq6044ps0');
  • 取得は session_start 関数をコールした でなければなりません.コールする前には(設定を行っていなければ) 空文字列 が返されます.
  • 設定は session_start 関数をコールする でなければなりません.
  • 設定を行えば, session_start コール時に強制的にそれをセッションIDとして利用させることが出来ます.
  • 設定を行わなければ, session_start コール時に $_COOKIE[session_name()] の値が利用されます.

以上を踏まえて,セッションファイルが保存されているパスは以下のように表現できるでしょう.

$path = session_save_path() . '/sess_' . $_COOKIE[session_name()];

session_start 関数をコールした であれば

$path = session_save_path() . '/sess_' . session_id();

とすることも出来ます.

他のユーザーと競合しないの?

上で述べたとおり, $_SESSIONセッションIDごとに用意されるので,競合する心配はありません.但し,

  • 悪意のあるユーザーが何らかの手段を使って他のユーザーのセッションIDを 盗み出しCookie: PHPSESSID=セッションID として送信した
  • スクリプト内で session_id 関数を使ってセッションIDを 固定して いる

これらに該当する場合は複数のユーザーでセッションが共有されることになるでしょう.

多重実行で自分自身と競合しないの?

セッションファイルは session_start 実行からスクリプト終了時まで 排他ロック されるので,競合の心配はありません.2回目以降のアクセスでまだ1回目の処理が終わっていないとき, session_start が記述されている行で1回目の処理が終わるまで待機し続けます

同一サーバー内にある他のサイトのセッションと競合しないの?

Cookieの項目で

$path

どこの階層から送信を行うかを表したパス.省略した場合は スクリプトが存在しているディレクトリ を指定したと見なされる.

と紹介しましたが,先ほどのセッションの項目では

session_start 関数が初めてコールされ,このときに
Set-Cookie: PHPSESSID=d41d8cd98f00b204e9800998ecf8427e; path=/
のように他と重複しないランダムな値が自動生成されてユーザー側に送られる.

として説明しました.これに違和感を感じられたのであればあなたは鋭い目をお持ちです.セッション用Cookieのデフォルトの$path'/'となっています.

これでも,shopping.example.comsearch.example.comのようにサブドメインで分けてある場合は特に問題はありません.しかし, example.com/shopping examle.com/search のようにパスで分けてある場合には衝突してしまいます.

これを解決するためには,Cookieが利用するパスを変えておく必要があります.

Cookieのパスを変更する
ini_set('session.cookie_path', '/foo/bar/baz');

有効期限ってどうなってるの?

有効期限と一口にいっても, 「サーバー側のセッションファイルの有効期限」「ユーザー側のクッキーファイルの有効期限」 の2種類が存在します.

サーバー側のセッションファイルの有効期限

次のエントリに非常に詳しい説明があります.以下,このエントリの内容を更に噛み砕いて掲載します.

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

この個々の値はphp.iniを編集して恒久的に変更,または ini_set 関数をコールして一時的に変更することが出来ます.設定は,session_start の実行よりも前にする必要があることに注意してください.

これらのうちいずれかを使う
// どのバージョンでも使える書き方
ini_set('session.gc_maxlifetime', 60 * 60 * 24 * 7);

// PHP7.0以降で使える書き方
session_start(['gc_maxlifetime' => 60 * 60 * 24 * 7]);

デフォルトでは次のような挙動になります.

  • 有効期限は最終アクセスから 24分間 (1440秒) となる.
  • 有効期限の過ぎたセッションファイルを毎回の実行ごとに検索して削除させるのは負荷がかかるので,デフォルトでは $\frac{1}{100}$ の確率でこの処理を行うようになっている.つまり有効期限切れのセッションファイルが発生してもすぐには削除が行われず,ある程度たまった段階でまとめて削除が行われるということを意味する.
  • 有効期限の過ぎたセッションファイルにアクセスがあった場合,有効期限が再度延長される.つまり有効期限切れのセッションファイルはその時点で無意味なものになるのではなく,単にいつ削除されても構わない状態になるということを意味する.

なお,この有効期限によるセッションファイルの削除は,サーバーごとに行われます.つまり,共用サーバーでは他のサイトの影響を受ける可能性があるということです.この問題を回避したい場合,有効期限の変更に加えて, session_save_path 関数または ini_set 関数で自分専用のディレクトリを設けてください.

ユーザー側のクッキーファイルの有効期限

以下のいずれかを実行します.設定は,session_start の実行よりも前にする必要があることに注意してください.

これらのうちいずれかを使う
// どのバージョンでも使える書き方
ini_set('session.cookie_lifetime', 60 * 60 * 24 * 7);

// PHP7.0以降で使える書き方
session_start(['cookie_lifetime' => 60 * 60 * 24 * 7]);

デフォルト値は 0 (ブラウザを閉じるまで) となっています.それ以外の場合,有効期限は最初のアクセスから指定した期間までとなります.後ほど紹介しますが,再アクセスで自動延長したい場合には少し工夫が必要です.

【注意】
setcookie 関数の $expire の表記の違いに注意してください.こちらは秒数ではなくタイムスタンプでの指定となっております.

ss (2014-02-23 at 05.03.21).png
ss (2014-02-23 at 05.04.02).png

どのように2つの有効期限が関連してくるか?

問題が発生する2通りのケースを考えてみましょう.

サーバー側にセッションファイルが残っているのに,セッション用のCookieが消滅してしまったとき

単純に,サーバー側のセッションファイルが 24分間 の期限と $\frac{1}{100}$ の確率の削除を待つだけです.

セッション用のCookieが送信されているのに,サーバー側のセッションファイルが既に削除されていたとき

送信されてきたセッションIDを元に,セッションファイルが再生成されます.また,もとから無効であった場合にもセッションファイルの生成は行われます.後者は特に セッションアダプション と呼ばれ,場合によっては脆弱性という扱いを受けます.

この挙動を変更することが出来るのがphp.iniの session.use_strict_mode ディレクティブです.デフォルトでは無効ですが,これを有効にすることで,無効なセッションIDであると判明した場合にセッションIDの再生成が行われ,ユーザーに向けて Set-Cookieヘッダー が新たに送信されます.ただし,このディレクティブにはほとんどセキュリティ対策の意味はありません.PHP7.1にてデフォルトで有効するというRFCもありましたが,賛成4対反対4で却下されています.

この機能が存在しないPHP5.5.1よりも古い環境で,手動で「セッションファイルが存在するか」を調べてアダプションを検知し,対策することは可能です.しかしこの対策はあまり意味が無いので,特に拘りが無ければ最初のままでいいかもしれません.

アダプション検知を付加したsession_start関数 (ついでにsession_start関数が吐きうるエラーに@演算子無しで対策している)
function safe_session_start($name = null) {
    if ($name !== null) {
        session_name($name);
    }
    $name = session_name();
    if (
        isset($_COOKIE[$name]) and
        !ctype_alnum($_COOKIE[$name]) ||
        !is_file(session_save_path() . '/sess_' . $COOKIE[$name])
    ) {
        unset($_COOKIE[$name]);
    }
    return session_start();
}

有効期限を長くするにはどうすればいいか?

上で紹介した2つの有効期限を同等に設定すれば可能です.

有効期限を1週間にする例
// どのバージョンでも使える書き方
ini_set('session.gc_maxlifetime', 60 * 60 * 24 * 7);
ini_set('session.cookie_lifetime', 60 * 60 * 24 * 7);
session_start();

// PHP7.0以降で使える書き方
session_start([
    'gc_maxlifetime' => 60 * 60 * 24 * 7,
    'cookie_lifetime' => 60 * 60 * 24 * 7,
]);

但し,これでは再アクセスによるCookieの有効期限の延長は行われません.これを実現したければ,自前で Set-Cookieヘッダー を送信させる必要があります.以下の記事でこれを実現した関数を紹介しています.

実はCookie以外にもセッションIDを受け取る手段がある!?

今までセッションについて,Cookieで個人を判別するものとして話を進めてきましたが,実は他にも手段があります.以下,優先順位の高い順に述べます.なお,ここで登場するphp.iniのディレクティブのうち,session.* のものは全て ini_set 関数で変更可能なものです.

1. $_COOKIE

  • session.use_cookies が有効な場合のみ採用されます.デフォルトは有効です.
  • ユーザー側に Set-Cookieヘッダー が自動的に送信されるようになります.

2. $_GET

  • session.use_only_cookies が無効な場合のみ採用されます.デフォルトはPHP5.2以前では無効,PHP5.3以降では有効です.
  • 以下の条件を全て満たしたとき,HTML中に存在するURLに対してクエリが自動で付加されます.

    • session.use_trans_sid が有効である.
    • 出力バッファリングを行っていなければならない.つまり ob_get_level 関数の返り値が1以上でなければならない. output_buffering が無効な場合は最初に ob_start 関数をコールしておく必要がある.
    • 相対URL(絶対パス・相対パス)でなければならない.外部サイトに送信してしまうリスクがあるため,絶対URLには付加されない
  • 手動で付加する場合には,定数 SID を使用します.'PHPSESSID=xxxxx' のような値が自動的に設定されています.

…とはいっても,PHP5.3以降デフォルトで使われるのは $_COOKIE 1つだけです.Cookieを使えないガラケーなんてもはやこの世に存在するかどうか怪しいですが,古代の端末向けにWebサイトを作らなければならない地雷案件があった場合に,設定を変更した上で他の手段を使うことがあるかもしれません.

3番以降の続きは付録に載せておきます.2番までは地雷案件で使うことがあったとしても,3番以降は本当に皆無でしょう…

「セッション固定攻撃」って何?どうやって対策するの?

攻撃対象

この攻撃手法においては,ログインする前から session_start 関数によりセッションが開始されているサイトが対象となります.多くのサイトがこの実装になっていると思います.

ログイン前からセッションを開始してログイン状態のチェックを行っている例
session_start();
if (isset($_SESSION['logined'])) {
    ....
}

以下に攻撃を受けるケースを述べます.

Cookie以外からのセッションIDを受け入れる場合

  1. 攻撃者のあなたはログインフォームにアクセスして,発行された自分用のセッションID PHPSESSID=xxxxx を入手します.
  2. http://example.com/login.php?PHPSESSID=xxxxx というURLをメールやTwitterなどの手段を使ってターゲットに踏ませます.この段階で,あなたとターゲットのセッションIDが共通なものになります.つまりサーバー側から見れば,あなたのWebブラウザとターゲットのWebブラウザが同じものと見なされます
  3. ターゲットがログイン状態になれば,あなたもターゲットのアカウントでログイン状態になります.攻撃成功です.

Cookie以外からのセッションIDを拒否しているが,XSS脆弱性がある場合

セキュリティコンサルタントをされている徳丸浩さんのTumblr記事で分かりやすい例が紹介されています.要はJavaScriptを実行できれば,クエリを踏ませなくてもセッションIDを共通なものに設定することが可能だということです.先ほど,

ただし,このディレクティブにはほとんどセキュリティ対策の意味はありません

とした理由もここで分かります.

対策方法

ではどうやって対策すればいいか?答えは単純明快,ログイン直後に次のコードを実行するだけです.

PHP5.1以降 (オプションで古いセッションファイルを自動的に削除可能)
session_regenerate_id(true);

session_regenerate_id 関数は,セッションIDを新規生成したものに乗り換える関数です.この関数は非常に重要なので,初めて見られた方は必ず覚えておいてください.上記のコードを実行した場合,以下の動作が行われます.

  1. セッションファイルを新しい名前にしてコピーする
  2. 古いセッションファイルを削除する
  3. ユーザーに新たに Set-Cookieヘッダー を送信する

要は,ログインしたタイミングで新しいセッションに乗り換えれば,誰かに目をつけられていてもそこで振り切れるということです.新しくセットした認証情報は古いセッションファイルには書き込まれないので,必ずしもオプションの指定が必須というわけではないですが,trueを指定しておくほうが一般的です。

なおこの関数を全てのページに使っていると,当然サーバーに対してものすごく負荷がかかる上にセッションの引継ぎがうまくいかない問題も発生してくるようです.ログイン直後に限定するようにしましょう.

セッションを破棄させるにはどうしたらいいか?

次のエントリに易しい説明があります.

ここではスポーツクラブの比喩が用いられていますが,完全に理解にするにはこの比喩はかえって妨げになると感じたので,事実のままに述べることにします.なお,セッション変数の特定のインデックスのみを削除したい場合は単純に

unset($_SESSION['key']);

としてください.以下,セッション変数全体を空にする操作についてのみ言及します.

(A) $_SESSION に空配列を代入する

$_SESSION = [];

サーバー側のメモリー上に存在している $_SESSION に格納された情報を全て削除します.

  • 現在のスクリプト実行が終了するまでの間はディスク上に存在している セッションファイル に書き込まれている情報はリセットされません.
  • ユーザー側の セッション用のCookie には何の影響も与えません.

(B) session_destroy 関数をコールする

session_destroy();

サーバー側のディスク上に存在している セッションファイル 自体を強制的に削除します.

  • 現在のスクリプト実行が終了するまでの間はメモリー上に存在している $_SESSION が保持している情報はリセットされません.
  • ユーザー側の セッション用のCookie には何の影響も与えません.

(C) 有効期限を過去にして setcookie 関数をコールする

setcookie(session_name(), '', 1, '/');

ユーザー側のセッション用のCookieを破棄させます. $pathを指定する必要があることに注意してください.

  • メモリー上に存在している $_SESSION が保持している情報には何の影響も与えません.
  • ディスク上に存在している セッションファイル に書き込まれている情報には何の影響も与えません.

A, B, C それぞれの役割を明確にして箇条書きにしてみました.これを全部行っておけば万全なのですが,全て行う必要があるかどうかと言えばそうではありません.

Aだけを行った場合

  • ディスク領域的には問題ないですが,0バイトのセッションファイルが残ります.
  • 甲さんがログアウトした後すぐに乙さんが同じWebブラウザからログインしてきた場合,甲さんが使っていたセッションファイルに乙さんの情報が上書きされます.甲さんが自分のセッションIDを控えていた場合,乙さんになりすますことが出来ます.セッション固定攻撃の場合も同様です.但し,ログイン直後に session_regenerate_id 関数をコールしている場合には安全です.

Bだけを行った場合

  • スクリプト終了時までに再度 $_SESSION にアクセスするようなコードを書いていた場合,アプリケーションが誤作動する可能性があります.
  • 甲さんがログアウトした後すぐに乙さんが同じWebブラウザからログインしてきた場合,甲さんが使っていたセッションファイルと同じファイル名で乙さんの情報が保存されます.甲さんが自分のセッションIDを控えていた場合,乙さんになりすますことが出来ます.セッション固定攻撃の場合も同様です.但し,ログイン直後に session_regenerate_id 関数をコールしている場合には安全です.

Cだけを行った場合

  • スクリプト終了時までに再度 $_SESSION にアクセスするようなコードを書いていた場合,アプリケーションが誤作動する可能性があります.
  • 不要になったセッションファイルが削除されるまでの間無駄にディスク領域を圧迫してしまうことになります.
  • A , B のように共用パソコンでのトラブルは防げますが,これだけではセッション固定攻撃までは防げません.但し,そもそもセッション固定攻撃を成立させなくしてしまえばいいので,やはりログイン直後に session_regenerate_id 関数をコールしている場合には安全です.

極論を言います. session_regenerate_id 関数のコールを行ってしまえば,後はどうしようと安全なのです.お好みで選択されても構わないし,万全を期して全部行っておいても問題ないです.個人的には, A , B は両方行い, C は行わなくてもよいという意見です.

逆に,セッションを破棄させる上でやってはいけないことを以下に挙げてみます.

(D) シンボルテーブルから $_SESSION を削除する

unset($_SESSION);

そのスクリプト実行中にはスーパーグローバル変数のセッションに関わる機能を全て使えなくしてしまいます.但し,セッションの情報が消失するわけではありません.次回のスクリプト実行時には復活します.

(E) $_SESSION という変数自体が配列以外になるように操作を加える

$_SESSION = null;

このケースは D と類似しているように思われますが,厳密には異なります.これを行った後に通常の代入操作を行う,例えば

$_SESSION = null;
$_SESSION['foo'] = 'bar';

とすると,セッションの機能は復活します.但し,それまでにあった情報は失われます.

$_SESSION = null;
$_SESSION = ['foo' => 'bar'];

のように書いた方が分かりやすいかもしれません.

「CSRF攻撃」って何?どうやって対策するの?

以下の記事の4章で具体例を交えて説明しています.

リンク先の記事の中で

原理や対策方法について詳しく説明すると難解になってくるので,ここでは対策方法だけを簡単にまとめておきます.興味のある方は各自調べてみてください.

と述べていますが,本記事を最後まで読まれた方なら自然と理解が進むと思います.対策の内容をまとめると,「ページを表示している本人しか知らないセッションIDを元にした値を埋め込んでおくことで,本人の意図通りにフォームが送信されたかどうかわかる」というところです. (但し,XSSなど別の脆弱性がある場合にはCSRF対策は機能しない)



付録

リクエストパラメータのパース規則

$_GET $_POST $_COOKIE

  • 1つの要素は キー=値 という構造を取る.
  • 同じキーが複数回現れた場合,後に出現したものが優先される.
  • キー または キー= とした場合,値は 空文字列 であると見なされる.
  • キーに存在する ._ に置換される.
  • キーが 空文字列 であるものは無視される.
  • パーセントエンコーディング に関して,デコード可能な部分はデコードし,不可能な部分はそのまま受け取る.つまり可能な限りデコードしようとする.キーと値どちらに対しても同様.
  • + はパーセントエンコーディングの %20 と同様に 半角スペース に置換される.
  • データ型は 文字列配列 の2通りしかない.
  • [] をキーに添えることで 配列 が表現できるが,開きカッコと閉じカッコの数が合わない場合,以下の処理のどれかが適用される.バージョンによって挙動が異なる可能性があり,個人的には未定義として扱ったほうがいいように思うので詳細は割愛する.
    • 外側を優先してペアとしてパースし,残った内側の [ を伴う部分は通常の文字列として扱う.
    • 無視される.
    • _ に置換される.

$_GET

  • 区切り文字には & が使われる.
  • パーセントエンコードされていない 半角スペース改行NULLバイト の混入は一切認められない.
  • 他にも使用不可能な文字がいくつかあるが紹介は割愛する.基本的にパーセントエンコードが必須だと思ったほうがいい.
デコード前 (見やすいように改行を入れています)
raw_text=あ&
encoded_text=%E3%81%82&
raw_percent=%&
encoded_percent=%25&
raw_equals===&
encoded_equals=%3D%3D&
raw_array[]=value&
encoded_array%5B%5D=value&
assoc[key]=value
デコード後 (代入のイメージ)
$_GET = [
    'raw_text' => 'あ',
    'encoded_text' => 'あ',
    'raw_percent' => '%',
    'encoded_percent' => '%',
    'raw_equals' => '==',
    'encoded_equals' => '==',
    'raw_array' => [
        0 => 'value',
    ],
    'encoded_array' => [
        0 => 'value',
    ],
    'assoc' => [
        'key' => 'value',
    ],
];

$_POST

  • 区切り文字には & が使われる.
  • パーセントエンコードされていない 半角スペース改行NULLバイト の混入は認められる.但し…
    • キーに存在する 半角スペース のみ _ に置換される.
    • キーに存在する NULLバイト のみ終端文字として扱われる 手前で切り捨てられる
デコード前 (改行は実際に入れます)
A = B &
C
= D
&E
 = F
デコード後 (代入のイメージ)
$_POST = [
    "A_" => " B ",
    "\nC\n" => " D\n",
    "E\n_" => " F",
];

$_COOKIE

  • 区切り文字には ; が使われる.
  • Studying HTTP に以下のような記述があったが,手元の Apache/2.4.4 (Win32) OpenSSL/0.9.8y PHP/5.4.16 で試したみたところ非対応のようであった.

RFC 2109 のクッキーでは, ; の他に , も使用できるようになりました.サーバは将来の互換性のために区切り子として , も受け入れるべきです.

  • パーセントエンコードされていない 半角スペース の混入は認められる.但し…
    • Cookie:または;「半角スペース以外の文字」 の間の半角スペースは無視される.
    • 「半角スペース以外の文字」= の間の半角スペースは _ に置換される.
  • パーセントエンコードされていない 改行NULLバイト の混入は一切認められない.
デコード前
Cookie: A = B ; C = D ; . = . ; E ; F
デコード後 (代入のイメージ)
$_COOKIE = [
    'A_' => ' B ',
    'C_' => ' D ',
    '__' => ' . ',
    'E_' => '',
    'F' => '',
];

使ってはいけないセッション関数!?

session_register 関数

PHPの歴史のお話をします.実は太古のPHP4.0まではスーパーグローバル変数が実装されておらず,以下のような変数を使う必要がありました.これらはただグローバルスコープに存在している変数であり,任意のスコープからアクセスすることができる変数ではありませんでした.

$HTTP_GET_VARS
$HTTP_POST_VARS
$HTTP_COOKIE_VARS
$HTTP_SESSION_VARS

これらはスーパーグローバルでない上に変数名が非常に長く,かなり使い勝手が悪いです.そこで,PHP5.3まではリクエストされた配列をローカル変数として自動展開する,つまり

extract($HTTP_COOKIE_VARS, EXTR_SKIP);
extract($HTTP_POST_VARS, EXTR_SKIP);
extract($HTTP_GET_VARS, EXTR_SKIP);

に該当する処理をPHPスクリプト実行開始時にグローバルスコープで自動的に行う register_globals という機能が存在していました.前者3つに関してはリクエストを送るユーザー側が展開する変数を決定することになっていました.これは非常に危険な機能であるとの批判が集中したため,PHP5.4で削除されています.

セッションに関しては session_register 関数を使ってプログラマ側が手動登録して参照状態にする仕組みになっていました.つまり,指定したものに関してだけ

extract($HTTP_SESSION_VARS, EXTR_OVERWRITE | EXTR_REFS);

が行われるということです.いくらは register_globals よりはマシですが,これも設計上好ましくないという理由で,PHP5.4にて削除されています.したがって,これらの関数は使ってはいけない,というか使うことができません.

session_set_cookie_params 関数

この関数はセッション用Cookieの挙動を制御する関数ですが,たとえば第3引数の値を設定するためには第1引数と第2引数も渡さなければならないという致命的な欠陥があり,非常に使い勝手が悪いです.

httponlyを有効にする (session_set_cookie_paramsを使う場合)
// 冗長
$p = session_get_cookie_params();
session_set_cookie_params($p['lifetime'], $p['path'], $p['domain'], $p['secure'], true);

// 多少はマシ
$p = session_get_cookie_params();
$p['httponly'] = true;
call_user_func_array('session_set_cookie_params', $p);

// PHP5.6以降,array_valuesがいるのがつらい
$p = session_get_cookie_params();
$p['httponly'] = true;
session_set_cookie_params(...array_values($p));

記事中で紹介しているとおり, ini_set などを用いる方法で十分です.

httponlyを有効にする (ini_setを使う場合)
ini_set('session.cookie_httponly', true);

session_unset 関数

次の2つの操作は等価ですが,後者の方を利用するようにかつてマニュアルが推奨していました.

session_unset();
$_SESSION = [];

今は方針が変わっているみたいですが,敢えて関数を利用する必要はないかと思います.

セッションの個人識別用パラメータ完全版 (優先度順)

1. $_COOKIE

  • session.use_cookies が有効な場合のみ採用されます.デフォルトは有効です.
  • ユーザー側に Set-Cookieヘッダー が自動的に送信されるようになります.

2. $_GET

  • session.use_only_cookies が無効な場合のみ採用されます.デフォルトはPHP5.2以前では無効,PHP5.3以降では有効です.
  • 以下の条件を全て満たしたとき,HTML中に存在するURLに対してクエリが自動で付加されます.

    • session.use_trans_sid が有効である.
    • 出力バッファリングを行っていなければならない.つまり ob_get_level 関数の返り値が1以上でなければならない. output_buffering が無効な場合は最初に ob_start 関数をコールしておく必要がある.
    • 相対URL(絶対パス・相対パス)でなければならない.外部サイトに送信してしまうリスクがあるため,絶対URLには付加されない
  • 手動で付加する場合には,定数 SID を使用します.'PHPSESSID=xxxxx' のような値が自動的に設定されています.

絶対URLに手動で付加する例
<a href="http://example.com/test.php?<?=htmlspecialchars(SID, ENT_QUOTES, 'UTF-8')?>">Test</a>

3. $_POST

  • session.use_only_cookies が無効な場合のみ採用されます.デフォルトはPHP5.2以前では無効,PHP5.3以降では有効です.
  • 送信は全て手動で行う必要があります.

4. $_SERVER['REQUEST_URI']

http://example.com/PHPSESSID=xxxxx/test.php のように自由な形で埋め込むことを許可します.

  • session.use_only_cookies が無効な場合のみ採用されます.デフォルトはPHP5.2以前では無効,PHP5.3以降では有効です.
  • 送信は全て手動で行う必要があります.

5. $_SERVER['HTTP_REFERER']

リファラーに対しても2と4に関するチェックが行われます.

  • session.use_only_cookies が無効な場合のみ採用されます.デフォルトはPHP5.2以前では無効,PHP5.3以降では有効です.
  • session.referer_check にURLに含まれるべき自サイト特有の文字列を指定します.こうすることで外部サイトのリファラーと区別することが出来ます.デフォルトは空文字列であり,このディレクティブは事実上無効となっています.このディレクティブにはほとんどセキュリティ対策の意味はありません