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

# 予備知識

## スーパーグローバル変数とは？

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

- [PHP Manual - 変数のスコープ](http://www.php.net/manual/ja/language.variables.scope.php)
- [PHP Manual - スーパーグローバル](http://www.php.net/manual/ja/language.variables.superglobals.php)

## HTTPとは？

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

- [Qiita - 【PHP超入門】HTTP（GET・POST）について](http://qiita.com/7968/items/4bf4d6f28284146c288f)<br>[Qiita - 【PHP超入門】Cookieとセッションについて](http://qiita.com/7968/items/ce03feb17c8eaa6e4672)<br>@7968さんによるQiitaの記事です．右も左もわからない人はまずこれで．<br><br>

- [とほほのWWW入門 - HTTP入門](http://www.tohoho-web.com/ex/http.htm)
最初の記事をもう少し体系的にまとめた感じです．ただし，Cookieに関する情報が欠落しているところが少し惜しいです…<br><br>

- [Studying HTTP](http://web.archive.org/web/20140303140503/http://www.studyinghttp.net/)<br>技術文書を読むことに慣れている人にはこちらが適当でしょう．厳密な定義が読みやすくまとめられています．但し，素晴らしいサイトであったにも関わらず，ドメイン自体が消滅してしまっているので，WebArchiveでの閲覧となります．<br><br>

- [Live HTTP Headers](https://addons.mozilla.org/ja/firefox/addon/live-http-headers/)<br>Firefoxのアドオンです．実際にこういった通信の内容をブラウザ上で確認したい場合などに活用してください．

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

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

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

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

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

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

```text:クエリ―ストリングの場合
A=B&C[D]=E
```

```text:Cookieヘッダーの場合
Cookie: A=B; C[D]=E
```

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

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

基本はこれだけで十分です．<ins>PHPはこれらの文字列を解析し，もとの配列に復元する仕事をしています</ins>．細かなパース規則については[付録](#%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E3%83%91%E3%83%BC%E3%82%B9%E8%A6%8F%E5%89%87)にしておきましたので，厳密にいろいろ試したい人は読んでください．

## PHPがWebブラウザからパラメータを受け取る方法 (基本編)
 
以下の2点に留意してお読みください．

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

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

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

#### <font color="silver">送信側</font>

```html:リンクをクリックするとリクエストが実行される
<a href="http://example.com/test.php?a=b">Test</a>
```

```html:フォームを送信するとリクエストが実行される
<form method="get" action="http://example.com/test.php">
<input type="hidden" name="a" value="b">
<input type="submit" value="送信">
</form>
```

```http:リクエストヘッダー
GET /test.php?a=b HTTP/1.1
Host: example.com
Connection: Close

```

#### <font color="silver">受信側</font>

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

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

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

- [isset](http://www.php.net/manual/ja/function.isset.php) でチェックする
- `??`演算子でチェックする (PHP7.0以降)
- [filter_input](http://www.php.net/manual/ja/function.filter-input.php) 関数経由で取得する

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

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

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

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

#### <font color="silver">送信側</font>

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

##### `Content-Length`

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

##### `Content-Type`

[application/x-www-form-urlencoded](http://www.wdic.org/w/WDIC/application/x-www-form-urlencoded) 固定です．

```html:フォームを送信するとリクエストが実行される
<form method="post" action="http://example.com/test.php">
<input type="hidden" name="a" value="b">
<input type="submit" value="送信">
</form>
```

```http:リクエストヘッダー
POST /test.php HTTP/1.1
Host: example.com
Content-Length: 3
Content-Type: application/x-www-form-urlencoded
Connection: Close

a=b
```

#### <font color="silver">受信側</font>

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

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

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

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

### Cookieヘッダーを送信する

#### <font color="silver">Cookieを保存させる</font>

[setcookie](http://www.php.net/manual/ja/function.setcookie.php) 関数または [setrawcookie](http://www.php.net/manual/ja/function.setrawcookie.php) 関数を使うと，ブラウザに対して簡単に整形された **Set-Cookieヘッダー** を送信することが出来ます．前者は<ins>値のみパーセントエンコード</ins>し，後者はキー・値ともそのままセットします．

```php:PHPでこれを実行する
setcookie('a', 'b');
```

```text:Webブラウザに送信されるSet-Cookieヘッダー&#32;<br>(基本形であって，実際にはこれの後ろにいろいろな情報が付加される)
Set-Cookie: a=b
```

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

##### `$expire`

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

- 省略または `0` を指定した場合，Webブラウザを閉じるまでが有効期限となる．
- `0` より大きく現在より小さい値（過去）を指定した場合，<ins>即座にWebブラウザで削除が行われる</ins>．但し， **共通な `$path` `$domain`** を指定していなければならない．

```php:よく見かける例
setcookie('foo', '', time() - 3600);
```

```php:実はこれでもOK
setcookie('foo', '', 1);
```

##### `$path`

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

##### `$domain`

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

##### `$secure`

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

##### `$httponly`

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

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

```php:(実行例)&#32;PHPコード
setcookie('a', 'b', 992174400, '/foo/bar', 'example.com', false, true);
```

```text:(実行結果)&#32;Set-Cookieヘッダー
Set-Cookie: a=b; expires=Sun, 10-Jun-2001 12:00:00 GMT; path=/foo/bar; domain=example.com; httponly
```

#### <font color="silver">Cookieを受信する</font>

文字列のままのCookieは<ins>頭の</ins>**`Cookie: `**<ins>を除いて</ins> **`$_SERVER['HTTP_COOKIE']`** に格納されます．

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

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

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

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

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

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

### URLにパス情報を含める

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

#### <font color="silver">送信側</font>

```html:リンクをクリックするとリクエストが実行される
<a href="http://example.com/test.php/foo/bar">Test</a>
```

```http:リクエストヘッダー
GET /test.php/foo/bar HTTP/1.1
Host: example.com
Connection: Close

```

#### <font color="silver">受信側</font>

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

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

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

**【注意】**
`+` は半角スペースに置換され**ません**． [rawurldecode](http://www.php.net/manual/ja/function.setcookie.php) 関数相当の処理です．

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

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

#### <font color="silver">送信側</font>

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

##### `Content-Length`

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

##### `Content-Type`

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

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

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

送信するVALUE属性値
```

```ファイルであることを明示するパラメータ
--境界文字列
Content-Disposition: form-data; name="NAME属性値"; filename="ファイル名"
Content-Type: MIMEタイプ（ブラウザが自動判定）

送信するファイルデータ
```

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

```text:終端行
--境界文字列--
```

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

```html:フォームを送信するとリクエストが実行される
<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>
```

```http:リクエストヘッダー
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--
```

#### <font color="silver">受信側</font>

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

- [Qiita - ファイルアップロードの例外処理はこれぐらいしないと気が済まない](http://qiita.com/mpyw/items/939964377766a54d4682)

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

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

```html:フォームを送信するとリクエストが実行される
<form method="post" action="http://example.com/test.php?a=b">
<input type="hidden" name="c" value="d">
<input type="submit" value="送信">
</form>
```

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

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

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

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

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

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

- [PHP Manual - file_get_contents](http://www.php.net/manual/ja/function.file-get-contents.php)
- [PHP Manual - Client URL Library](http://www.php.net/manual/ja/book.curl.php)

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

- [Qiita - PHPネイティブのDOMによるスクレイピング入門](http://qiita.com/mpyw/items/c0312271819baee09132)

# セッション関連あれこれ

ここからは [セッション](http://www.php.net/manual/ja/book.session.php) を扱っていきます．前半で紹介した知識を前提としている箇所もあるので，忘れてしまったら戻って読み返してみてください．

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

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

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

```php:【B】所持金MAX！！
$_COOKIE['money'] = '99999999';
```

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

[Qiita - $_GET, $_POSTなどを受け取る際の処理](http://qiita.com/mpyw/items/2f9955db1c02eeef43ea)

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

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

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

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

## 基本的な使い方は？

[session_start](http://www.php.net/manual/ja/function.session-start.php) 関数により `$_SESSION` 変数を準備し，あとは値の取得/代入といった操作を実行すればいいだけです．

```html+php:マニュアルより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](http://www.php.net/manual/ja/function.session-start.php) の前に `@` をつけているのは，不正なリクエストが来ると **E_NOTICE** 更には **E_WARNING** レベルのエラーも発生し得るからです．PHP言語の欠陥ですね，これは…．

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

以降では，`@`をつけないことにします．

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

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

```php:三項演算子を用いた例
$data = isset($_POST['data']) ? $_POST['data'] : '';
```

```php:filter_input関数を活用して型チェックまで行う例
$data = (string)filter_input(INPUT_POST, 'data');
```

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

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

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

(但し， [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数のエラーの発生を `@` 演算子無しに防ぐために `$_COOKIE` を操作することは例外的に認められるでしょう．このコードは後ほど出てきます．)

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

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

```php:GOOD
$_SESSION['nine'] // 通常の文字列
$_SESSION['010']  // 8進表記文字列
```

```php:BAD
$_SESSION[8]   // 10進表記整数
$_SESSION[010] // 8進表記整数(実質的には上記と等しい)
$_SESSION['8'] // 10進表記文字列
$_SESSION['0'] // 10進表記文字列
```

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

`$_GET` `$_POST` `$_COOKIE` `$_REQUEST` などは **文字列** と **配列** しか扱えませんでしたが，`$_SESSION` はリソース型以外の **任意のデータ** を保存することが出来ます．但しオブジェクトを保存する場合，そのクラスの定義が [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数のコールよりも<ins>前に</ins>行われている必要があります．

```php:GOOD
require 'Test.class.php';
session_start();
```

```php:BAD
session_start();
require 'Test.class.php';
```

## Cookieとどう違うの？

既に述べたように

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

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

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

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

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

以降は繰り返しです．

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

- [PHPで確認するクッキーとセッションメモ - 訪問回数のカウント](http://qiita.com/kure/items/8539532ac097bf93fd24)

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

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

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

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

- php.iniの編集
- [ini_set](http://www.php.net/manual/ja/function.ini-set.php) 関数または [ini_get](http://www.php.net/manual/ja/function.ini-get.php) 関数
- [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数の第1引数 (PHP7.0以降のみで使えるオプション)
- 専用の関数

### セッション用Cookieの名前

```php:これらのうちいずれかを使う
// 取得
$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](http://www.php.net/manual/ja/function.session-start.php) 関数をコールする **前** でなければなりません．
- デフォルト値は空であり，その場合 [sys_get_temp_dir](http://www.php.net/manual/ja/function.sys-get-temp-dir.php) 関数の返り値で取れる，システム既定のテンポラリディレクトリが使われます．

### セッション用Cookieの名前

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

// 設定
session_name('MYSESSID');
ini_set('session.name', 'MYSESSID');
session_start(['name' => 'MYSESSID']); // PHP7.0以降
```

- 取得は常に可能です．
- 設定は [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数をコールする **前** でなければなりません．
- デフォルト値があらかじめ **`'PHPSESSID'`** として設定されています．

### セッション用Cookieの値

```php
// 取得
$id = session_id();

// 設定
session_id('vmtflhhks040679m9rq6044ps0');
```

- 取得は [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数をコールした **後** でなければなりません．コールする前には(設定を行っていなければ) **空文字列** が返されます．
- 設定は [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数をコールする **前** でなければなりません．
- 設定を行えば， [session_start](http://www.php.net/manual/ja/function.session-start.php) コール時に強制的にそれをセッションIDとして利用させることが出来ます．
- 設定を行わなければ， [session_start](http://www.php.net/manual/ja/function.session-start.php) コール時に **`$_COOKIE[session_name()]`** の値が利用されます．

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

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

[session_start](http://www.php.net/manual/ja/function.session-start.php) 関数をコールした **後** であれば

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

とすることも出来ます．

## 他のユーザーと競合しないの？

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

- 悪意のあるユーザーが何らかの手段を使って他のユーザーのセッションIDを **盗み出し** ， `Cookie: PHPSESSID=セッションID` として送信した
- スクリプト内で [session_id](http://www.php.net/manual/ja/function.session-id.php) 関数を使ってセッションIDを **固定して** いる

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

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

セッションファイルは [session_start](http://www.php.net/manual/ja/function.session-start.php) 実行からスクリプト終了時まで **排他ロック** されるので，競合の心配はありません．2回目以降のアクセスでまだ1回目の処理が終わっていないとき， [session_start](http://www.php.net/manual/ja/function.session-start.php) が記述されている行で<ins>1回目の処理が終わるまで待機し続けます</ins>．

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

Cookieの項目で

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

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

> [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数が初めてコールされ，このときに<br />`Set-Cookie: PHPSESSID=d41d8cd98f00b204e9800998ecf8427e; path=/`<br />のように他と重複しないランダムな値が自動生成されてユーザー側に送られる．

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

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

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

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

## 有効期限ってどうなってるの？

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

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

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

- [Pentan.info - セッションの有効期間とか設定とか挙動とかを調べました](http://pentan.info/php/session_gc.html)

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

この個々の値はphp.iniを編集して恒久的に変更，または [ini_set](http://www.php.net/manual/ja/function.ini-set.php) 関数をコールして一時的に変更することが出来ます．設定は，[session_start](http://php.net/manual/ja/function.session-start.php) の実行よりも前にする必要があることに注意してください．

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

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

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

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

なお，この有効期限によるセッションファイルの削除は，サーバーごとに行われます．つまり，共用サーバーでは他のサイトの影響を受ける可能性があるということです．この問題を回避したい場合，有効期限の変更に加えて， [session_save_path](http://www.php.net/manual/ja/function.session-save-path.php) 関数または [ini_set](http://www.php.net/manual/ja/function.ini-set.php) 関数で自分専用のディレクトリを設けてください．

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

以下のいずれかを実行します．設定は，[session_start](http://php.net/manual/ja/function.session-start.php) の実行よりも前にする必要があることに注意してください．

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

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

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

【注意】
[setcookie](http://www.php.net/manual/ja/function.setcookie.php) 関数の **`$expire`** の表記の違いに注意してください．こちらは秒数ではなく**タイムスタンプ**での指定となっております．

![ss (2014-02-23 at 05.03.21).png](https://qiita-image-store.s3.amazonaws.com/0/25060/197fd64f-e3d0-e1d0-cc96-b8647a9aa849.png)
![ss (2014-02-23 at 05.04.02).png](https://qiita-image-store.s3.amazonaws.com/0/25060/a2ccd506-0691-700e-9ef5-70014925b818.png)

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

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

#### <font color="silver">サーバー側にセッションファイルが残っているのに，セッション用のCookieが消滅してしまったとき</font>

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

#### <font color="silver">セッション用のCookieが送信されているのに，サーバー側のセッションファイルが既に削除されていたとき</font>

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

この挙動を変更することが出来るのがphp.iniの [session.use_strict_mode](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-strict-mode) ディレクティブです．デフォルトでは無効ですが，これを有効にすることで，無効なセッションIDであると判明した場合にセッションIDの再生成が行われ，ユーザーに向けて **Set-Cookieヘッダー** が新たに送信されます．ただし，<ins>このディレクティブにはほとんどセキュリティ対策の意味はありません</ins>．PHP7.1にて[デフォルトで有効するというRFC](https://wiki.php.net/rfc/session-use-strict-mode)もありましたが，賛成4対反対4で却下されています．

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

```php:アダプション検知を付加したsession_start関数&#32;(ついでに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つの有効期限を同等に設定すれば可能です．

```php:有効期限を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,
]);
```

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

- [Qiita - ぼくのかんがえたさいきょうのsession_start](http://qiita.com/mpyw/items/7f4772e4d4d360fc100c)

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

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

### 1. `$_COOKIE`

- [session.use_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-cookies) が有効な場合のみ採用されます．デフォルトは有効です．
- ユーザー側に **Set-Cookieヘッダー** が自動的に送信されるようになります．

### 2. `$_GET`

- [session.use_only_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-only-cookies) が無効な場合のみ採用されます．デフォルトは<ins>PHP5.2以前では無効</ins>，PHP5.3以降では有効です．
- 以下の条件を全て満たしたとき，HTML中に存在するURLに対してクエリが自動で付加されます．
  - [session.use_trans_sid](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-trans-sid) が有効である．
  - 出力バッファリングを行っていなければならない．つまり [ob_get_level](http://www.php.net/manual/ja/function.ob-get-level.php) 関数の返り値が1以上でなければならない． [output_buffering](http://www.php.net/manual/ja/outcontrol.configuration.php#ini.output-buffering) が無効な場合は最初に [ob_start](http://www.php.net/manual/ja/function.ob-start.php) 関数をコールしておく必要がある．
  - 相対URL(絶対パス・相対パス)でなければならない．外部サイトに送信してしまうリスクがあるため，<ins>絶対URLには付加されない</ins>．

- 手動で付加する場合には，定数 **`SID`** を使用します．`'PHPSESSID=xxxxx'` のような値が自動的に設定されています．

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

3番以降の続きは[付録](#%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%80%8B%E4%BA%BA%E8%AD%98%E5%88%A5%E7%94%A8%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E5%AE%8C%E5%85%A8%E7%89%88-%E5%84%AA%E5%85%88%E5%BA%A6%E9%A0%86)に載せておきます．2番までは地雷案件で使うことがあったとしても，3番以降は本当に皆無でしょう…

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

### 攻撃対象

この攻撃手法においては，ログインする前から [session_start](http://www.php.net/manual/ja/function.session-start.php) 関数によりセッションが開始されているサイトが対象となります．多くのサイトがこの実装になっていると思います．

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

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

#### <font color="silver">Cookie以外からのセッションIDを受け入れる場合</font>

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

#### <font color="silver">Cookie以外からのセッションIDを拒否しているが，XSS脆弱性がある場合</font>

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

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

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

- [徳丸浩のtumblr - セッションアダプションがなくてもセッションフィクセイション攻撃は可能](http://tumblr.tokumaru.org/post/37676352092/session-adoption-and-session-fixation)

### 対策方法

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

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

[session_regenerate_id](http://php.net/manual/ja/function.session-regenerate-id.php) 関数は，セッションIDを新規生成したものに乗り換える関数です．この関数は非常に重要なので，初めて見られた方は必ず覚えておいてください．上記のコードを実行した場合，以下の動作が行われます．

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

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

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

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

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

- [プログラマはサイコロを振らない - PHPでセッションを完全に破棄する方法](http://d.hatena.ne.jp/Kappuccino/20080726/1217049706)

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

```php
unset($_SESSION['key']);
```

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

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

```php
$_SESSION = [];
```

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

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

### (B) [session_destroy](http://www.php.net/manual/ja/function.session-destroy.php) 関数をコールする

```php
session_destroy();
```

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

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

### (C) 有効期限を過去にして [setcookie](http://www.php.net/manual/ja/function.setcookie.php) 関数をコールする

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

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

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

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

### Aだけを行った場合

- ディスク領域的には問題ないですが，0バイトのセッションファイルが残ります．
- 甲さんがログアウトした後すぐに乙さんが同じWebブラウザからログインしてきた場合，甲さんが使っていたセッションファイルに乙さんの情報が上書きされます．甲さんが自分のセッションIDを控えていた場合，乙さんになりすますことが出来ます．セッション固定攻撃の場合も同様です．但し，ログイン直後に <ins>[session_regenerate_id](http://www.php.net/manual/ja/function.session-regenerate-id.php) 関数をコールしている場合には安全</ins>です．

### Bだけを行った場合

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

### Cだけを行った場合

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

極論を言います． [session_regenerate_id](http://www.php.net/manual/ja/function.session-regenerate-id.php) 関数のコールを行ってしまえば，後はどうしようと安全なのです．お好みで選択されても構わないし，万全を期して全部行っておいても問題ないです．個人的には， **A** , **B** は両方行い， **C** は行わなくてもよいという意見です．

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

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

```php
unset($_SESSION);
```

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

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

```php
$_SESSION = null;
```

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

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

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

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

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

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

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

- [Qiita - 【PHP初心者向け】セキュアな掲示板を最小構成から作る](http://qiita.com/mpyw/items/2c54d0ea95423bd88f60)

リンク先の記事の中で

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

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

--------
--------

# 付録

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

### `$_GET` `$_POST` `$_COOKIE`

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

### `$_GET`

- 区切り文字には **`&`** が使われる．
- パーセントエンコードされていない **半角スペース** と **改行** と **NULLバイト** の混入は一切認められない．
- 他にも使用不可能な文字がいくつかあるが紹介は割愛する．基本的にパーセントエンコードが必須だと思ったほうがいい．

```text:デコード前&#32;(見やすいように改行を入れています)
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
```

```php:デコード後&#32;(代入のイメージ)
$_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バイト** の混入は<ins>認められる</ins>．但し…
  - キーに存在する **半角スペース** のみ **`_`** に置換される．
  - キーに存在する **NULLバイト** のみ終端文字として扱われる **（<ins>手前で切り捨てられる</ins>）** ．


```text:デコード前&#32;(改行は実際に入れます)
A = B &
C
= D
&E
 = F
```

```php:デコード後&#32;(代入のイメージ)
$_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** のクッキーでは， **`;`** の他に **`,`** も使用できるようになりました．サーバは将来の互換性のために区切り子として **`,`** も受け入れるべきです．

- パーセントエンコードされていない **半角スペース** の混入は<ins>認められる</ins>．但し…
  - **「`Cookie:`または`;`」** ～ **「半角スペース以外の文字」** の間の半角スペースは無視される．
  - **「半角スペース以外の文字」** ～ **`=`** の間の半角スペースは **`_`** に置換される．
- パーセントエンコードされていない **改行** と **NULLバイト** の混入は一切認められない．

```text:デコード前
Cookie: A = B ; C = D ; . = . ; E ; F
```

```php:デコード後&#32;(代入のイメージ)
$_COOKIE = [
    'A_' => ' B ',
    'C_' => ' D ',
    '__' => ' . ',
    'E_' => '',
    'F' => '',
];
```

## 使ってはいけないセッション関数！？

### [session_register](http://www.php.net/manual/ja/function.session-register.php) 関数

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

```php
$HTTP_GET_VARS
$HTTP_POST_VARS
$HTTP_COOKIE_VARS
$HTTP_SESSION_VARS
```

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

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

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

セッションに関しては [session_register](http://www.php.net/manual/ja/function.session-register.php) 関数を使って<ins>プログラマ側が手動登録して参照状態にする</ins>仕組みになっていました．つまり，指定したものに関してだけ

```php
extract($HTTP_SESSION_VARS, EXTR_OVERWRITE | EXTR_REFS);
```

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

### [session_set_cookie_params](http://php.net/manual/ja/function.session-set-cookie-params.php) 関数

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

```php:httponlyを有効にする&#32;(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](http://php.net/manual/ja/function.ini-set.php) などを用いる方法で十分です．

```php:httponlyを有効にする&#32;(ini_setを使う場合)
ini_set('session.cookie_httponly', true);
```

### [session_unset](http://www.php.net/manual/ja/function.session-unset.php) 関数

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

```php
session_unset();
```

```php
$_SESSION = [];
```

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

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

### 1. `$_COOKIE`

- [session.use_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-cookies) が有効な場合のみ採用されます．デフォルトは有効です．
- ユーザー側に **Set-Cookieヘッダー** が自動的に送信されるようになります．

### 2. `$_GET`

- [session.use_only_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-only-cookies) が無効な場合のみ採用されます．デフォルトは<ins>PHP5.2以前では無効</ins>，PHP5.3以降では有効です．
- 以下の条件を全て満たしたとき，HTML中に存在するURLに対してクエリが自動で付加されます．
  - [session.use_trans_sid](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-trans-sid) が有効である．
  - 出力バッファリングを行っていなければならない．つまり [ob_get_level](http://www.php.net/manual/ja/function.ob-get-level.php) 関数の返り値が1以上でなければならない． [output_buffering](http://www.php.net/manual/ja/outcontrol.configuration.php#ini.output-buffering) が無効な場合は最初に [ob_start](http://www.php.net/manual/ja/function.ob-start.php) 関数をコールしておく必要がある．
  - 相対URL(絶対パス・相対パス)でなければならない．外部サイトに送信してしまうリスクがあるため，<ins>絶対URLには付加されない</ins>．

- 手動で付加する場合には，定数 **`SID`** を使用します．`'PHPSESSID=xxxxx'` のような値が自動的に設定されています．

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

### 3. `$_POST`

- [session.use_only_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-only-cookies) が無効な場合のみ採用されます．デフォルトは<ins>PHP5.2以前では無効</ins>，PHP5.3以降では有効です．
- 送信は全て手動で行う必要があります．

### 4. `$_SERVER['REQUEST_URI']`

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

- [session.use_only_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-only-cookies) が無効な場合のみ採用されます．デフォルトは<ins>PHP5.2以前では無効</ins>，PHP5.3以降では有効です．
- 送信は全て手動で行う必要があります．

### 5. `$_SERVER['HTTP_REFERER']`

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

- [session.use_only_cookies](http://www.php.net/manual/ja/session.configuration.php#ini.session.use-only-cookies) が無効な場合のみ採用されます．デフォルトは<ins>PHP5.2以前では無効</ins>，PHP5.3以降では有効です．
- [session.referer_check](http://www.php.net/manual/ja/session.configuration.php#ini.session.referer-check) にURLに含まれるべき自サイト特有の文字列を指定します．こうすることで外部サイトのリファラーと区別することが出来ます．デフォルトは空文字列であり，このディレクティブは事実上無効となっています．<ins>このディレクティブにはほとんどセキュリティ対策の意味はありません</ins>．