とっても簡単なCSRF対策

  • 262
    いいね
  • 18
    コメント
この記事は最終更新日から1年以上が経過しています。

コメント欄の議論に関するまとめ

以下,XSS脆弱性が存在しない前提.この脆弱性があるとあらゆるCSRF対策がほとんど意味をなさなくなるので,まずここから潰しておくこと.

セッション固定攻撃に対する対策

  • ログイン後にsession_regenerate_idを必ず実行する.
  • ログアウト後にsession_destroyを必ず実行する.

CSRF攻撃に対する対策

  • セッションIDを抜かれることは原則的には無いが,セッションIDをそのままトークンに用いるのは避けたほうが無難.
  • ワンタイムトークンにはF5リロードでの誤作動を防止する意味合いもあるが,必ずしもワンタイム性が求められるわけではない.固定トークンでも十分なセキュリティは保証される.
  • トークンによる対策が施されていない場合,GETであってもPOSTであってもCSRFは実行可能なので,それは議論の対象ではない.

実装

以前はワンタイムトークン推奨にしていましたが,意向が変わってきたので固定トークンのサンプルに差し替えておきます.

クラス定義
class CsrfValidator {

    const HASH_ALGO = 'sha256';

    public static function generate()
    {
        if (session_status() === PHP_SESSION_NONE) {
            throw new \BadMethodCallException('Session is not active.');
        }
        return hash(self::HASH_ALGO, session_id());
    }

    public static function validate($token, $throw = false)
    {
        $success = self::generate() === $token;
        if (!$success && $throw) {
            throw new \RuntimeException('CSRF validation failed.', 400);
        }
        return $success;
    }

}

使い方

フォームに埋め込むとき
<input type="hidden" name="token" value="<?=CsrfValidator::generate()?>">
検証するとき(返り値チェックタイプ)
<?php

if (!CsrfValidator::validate(filter_input(INPUT_POST, 'token'))) {
    header('Content-Type: text/plain; charset=UTF-8', true, 400);
    die('CSRF validation failed.');
}

// 続きの処理

?>
<!DOCTYPE html>
...
検証するとき(例外タイプ)
<?php

try {

    CsrfValidator::validate(filter_input(INPUT_POST, 'token'), true);

    // 続きの処理…

} catch (\RuntimeException $e) {

    header('Content-Type: text/plain; charset=UTF-8', true, $e->getCode() ?: 500);
    die($e->getMessage());

}

?>
<!DOCTYPE html>
...