ぼくのかんがえたさいきょうのsession_start

# これは何？

- [リクエストパラメータ・セッションに関するまとめ](http://qiita.com/mpyw/items/7852213f478e8c5a2802)
- [とっても簡単なCSRF対策](http://qiita.com/mpyw/items/8f8989f8575159ce95fc)
- [PHPによる簡単なログイン認証いろいろ](http://qiita.com/mpyw/items/bb8305ba196f5105be15)
- [セキュアな掲示板を最小構成から作る](http://qiita.com/mpyw/items/2c54d0ea95423bd88f60)

いろいろセッションが絡む記事を書いてきましたが，この記事で紹介するものが以下のすべてを行える完成形です．

- 不正なセッションIDは無視して`session_start`によるエラー発生を防ぐ
- セッションの失効までの期限延長を行う (任意)
- **セッションの失効より短いスパンでセッションIDの再生成を行う (任意)**
- CSRFトークンの検証を行う

(ここから更に改善するとすればオブジェクト指向で書くぐらいですが，シンプルにしたかったので敢えて避けました)

# 関数定義

```php:super_session.php
<?php

// CSRFトークン生成に使うハッシュアルゴリズム
const CSRF_TOKEN_HASHALGO = 'sha256';

// セッションIDの更新時刻に使う $_SESSION のキー
const SESSION_UPDATETIME_KEY = '__NEXT_UPDATE__';

/**
 * 延長可能なセッションを開始する
 * 安全のため定期的にセッションIDの更新を行う
 * ついでに不正なセッションIDによるエラーの発生も防ぐ
 *
 * @param int $lifetime 最終アクセスから失効までの秒数 (0でセッションクッキー化)
 * @param int $updatetime 最終更新から次回更新までの秒数 (0で更新無し)
 * @param bool $extend 再アクセスでセッションを延長するかどうか
 * @param array $options PHP7.0以降から使えるsession_startの第1引数互換
 * @return bool 可否
 */
function super_session_start($lifetime = 0, $updatetime = 0, $extend = true, $options = [])
{
    // セッションファイルおよびクッキーの両方の有効期限を設定
    ini_set('session.gc_maxlifetime', $lifetime);
    ini_set('session.cookie_lifetime', $lifetime);
    
    // その他のオプションを設定
    foreach ($options as $key => $value) {
        ini_set("session.$key", $value);
    }
    
    // 不正なセッションIDの無効化とセッションの延長
    $name = session_name();
    if (isset($_COOKIE[$name])) {
        if (!ctype_alnum($_COOKIE[$name])) {
            unset($_COOKIE[$name]);
        } elseif ($lifetime > 0 && $extend) {
            setcookie(
                $name,
                $_COOKIE[$name],
                time() + $lifetime,
                ini_get('session.cookie_path'),
                ini_get('session.cookie_domain'),
                (bool)ini_get('session.cookie_secure'),
                (bool)ini_get('session.httponly')
            );
        }
    }
    
    // セッションの開始
    if (!session_start()) {
        return false;
    }

    // セッションIDの更新
    if ($updatetime > 0) {
        if (!isset($_SESSION[SESSION_UPDATETIME_KEY])) {
            $_SESSION[SESSION_UPDATETIME_KEY] = time() + $updatetime;
        } elseif ($_SESSION[SESSION_UPDATETIME_KEY] <= time()) {
            session_regenerate_id(true);
            $_SESSION[SESSION_UPDATETIME_KEY] = time() + $updatetime;
        }
    }
    
    return true;
}

/**
 * CSRFトークンから送信の有効性を検証する
 * セッションIDの更新が行われても対応できるように，リクエスト受理時のものを優先する
 * 
 * @param string $token 送信されてきたCSRFトークン
 * @return bool 可否
 */
function validate_token($token)
{
    return $token === hash(CSRF_TOKEN_HASHALGO, filter_input(INPUT_COOKIE, session_name()) ?: session_id());
}

/**
 * CSRFトークンを生成する
 * 
 * @return bool 可否
 */
function generate_token()
{
    return hash(CSRF_TOKEN_HASHALGO, session_id());
}
```

# サンプル

- 5秒ごとにセッションIDが変わります
- 60秒放置するとセッションが失効し，「First Access」の値が変わります
- セッションが失効しない限り，常にCSRF検証は成功します
- `httponly`属性をつけたので，ブラウザ拡張を除くJavaScriptからはセッションIDを取得できません

```php:index.php
<?php

require_once __DIR__ . '/super_session.php';

function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

date_default_timezone_set('Asia/Tokyo');

// 60秒放置で失効，5秒ごとにセッションID更新
super_session_start(60, 5, true, [
    'cookie_httponly' => true,
]);

if (!isset($_SESSION['First Access'])) {
    $_SESSION['First Access'] = date('Y-m-d H:i:s');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $msg = validate_token(filter_input(INPUT_POST, 'token')) ? 'Passed' : 'Fail';
} else {
    $msg = 'Submit this form';
}

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

?>
<!DOCTYPE html>
<title>Example</title>
<table border="1">
  <tr>
    <th>Session ID (server generated)</th>
    <td><?=h(session_id())?></td>
  </tr>
  <tr>
    <th>Session ID (you sent)</th>
    <td><?=h(filter_input(INPUT_COOKIE, session_name()))?></td>
  </tr>
  <tr>
    <th>First Access</th>
    <td><?=h($_SESSION['First Access'])?></td>
  </tr>
  <tr>
    <th>CSRF Token (server generated)</th>
    <td><?=h(generate_token())?></td>
  </tr>
  <tr>
    <th>CSRF Token (you sent)</th>
    <td><?=h(filter_input(INPUT_POST, 'token'))?></td>
  </tr>
  <tr>
    <th>CSRF Check</th>
    <td><?=h($msg)?></td>
  </tr>
</table>
<form action="" method="post">
    <input type="hidden" name="token" value="<?=h(generate_token())?>">
    <input type="submit">
</form>
```

# どういうときにセッションIDの自動更新が必要か？

ズバリ，**TLS暗号化が無いとき**．これに尽きます．

- しかし，最近は[Let's Encrypt](https://letsencrypt.org/)などの登場で気軽にTLSを導入できるようになってきているので，導入されていない方は前向きに導入を検討しましょう．
- もちろん暗号化の有無に関わらず，**ログイン成功直後**には行ってくださいね．

```php:(備考)&#32;コンテンツをすべてTLS暗号化している場合におすすめの設定
super_session_start(86400 * 5, 0, true, [
    'cookie_secure' => true,
    'cookie_httponly' => true,
]);
```