【2021/10/15 追記】
この記事は更新が停止されています。現在では筆者の思想が変化している面もありますので,過去の記事として参考程度にご覧ください。
CSRFおよびその対策の仕組みに関してはこちら↓
これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎 - Qiita
この記事は,PHPにおけるワンタイムトークンを用いた実装例を示すものです。執筆日が少々古いものになるのでご了承ください。
コメント欄の議論に関するまとめ
以下,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>
...