Edited at

とっても簡単なCSRF対策

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>
...