CSRFトークンの考え方
CSRFとは、ログイン済みの利用者のブラウザを悪用し、外部サイトから勝手にリクエストを送らせる攻撃です。フォームに秘密のトークンを埋めて照合し、自サイト由来かを確かめて防ぎます。
他サイトから勝手にPOSTされる攻撃
CSRFは、ログイン済みの利用者のブラウザを使って、攻撃者が用意した別サイトから勝手にリクエストを送らせる攻撃ですね。クッキーは自動で送られてしまうので、正規の利用者になりすまして設定変更や送金のような操作を実行されかねません。防ぐ発想はシンプルで、「そのフォームが確かに自サイトから来た」ことを証明する秘密の値を一緒に送らせる、というものです。
session_start();
// トークンをセッションに保存しておく
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
random_bytes で作った推測されにくい値をセッションに持たせ、同じ値をフォームの hidden にも埋め込みます。攻撃者はセッション内のトークンを知りようがないので、正しい値を送れないわけですね。
フォームに埋めて検証する
フォーム側には hidden フィールドとしてトークンを出力します。受け取り側では、送られてきた値とセッションの値が一致するかを確認します。ここの比較には後述の理由で hash_equals を使うのが定石ですね。
// フォーム出力
$token = $_SESSION['csrf_token'];
echo '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($token, ENT_QUOTES) . '">';
// 受信時の検証
$sent = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $sent)) {
http_response_code(419);
exit('CSRF検証に失敗しました');
}
ワンタイムにするか使い回すか
トークンをリクエストごとに使い捨てる方式にすると強度は上がりますが、戻るボタンや複数タブでトークンが食い違い、正規の操作まで弾いてしまうことがあります。実務ではセッション単位で一つ持ち、ログインのタイミングなど区切りで再生成する折衷がバランス良いと思いますね。SameSite クッキー属性を Lax か Strict にしておくと、そもそも他サイトからのクッキー送出を抑えられるので、トークンと合わせて多層で守るのがいい気がします。
// クッキー側でも防御を重ねる
session_set_cookie_params([
'samesite' => 'Lax',
'secure' => true,
'httponly' => true,
]);
session_start();
まとめ
CSRF対策の核は「自サイト由来であることを、外から知り得ない値で証明させる」ことだと思います。random_bytes で作ってセッションに置き、hash_equals で照合、そして SameSite クッキーで層を重ねる。この組み合わせにしておけば、実務で困ることはほぼないという気がします。ワンタイムかどうかは利便性とのトレードオフで決めるといいですね。
よくある質問
Q. GETリクエストにもCSRF対策は必要ですか?
A. 状態を変えない参照だけのGETなら基本的に不要です。データを変更する操作はPOSTにまとめ、そこにトークン検証を入れるのが素直だと思います。
Q. SameSiteクッキーを設定すればトークンは要りませんか?
A. SameSiteは有力な防御ですが、ブラウザ依存や設定漏れの余地があります。トークンと併用して多層で守るほうが安心だと思いますね。