リンドくん先生、「CSRF攻撃」ってたまに聞くんですけど、これって何なんですか?
たなべ確かに名前だけ聞くと難しそうだよね。でも実は、あなたのログイン状態を悪用される攻撃なんだ。
例えば、SNSにログインしたまま悪意あるサイトを見ると、知らないうちに投稿されちゃう...みたいなことが起こる可能性があるんだよ。
リンドくんえっ、怖いですね!でもどうやって防ぐんですか?
たなべそこで登場するのがCSRFトークンという仕組みなんだ。今日はこれについて、初心者の方でもわかるように詳しく説明していくよ。
Webアプリケーションを開発していると、必ず直面するのがセキュリティの問題です。
その中でもCSRF(Cross-Site Request Forgery)攻撃は、開発者が理解しておくべき重要な脅威の一つです。
CSRF攻撃は日本語で「クロスサイトリクエストフォージェリ」と呼ばれ、ユーザーの意図しない操作を勝手に実行させてしまう攻撃のことを指します。
ログイン状態を悪用されることで、パスワード変更や商品購入、SNSへの投稿など、さまざまな操作が勝手に行われてしまう可能性があるのです。
この記事では、CSRF攻撃の仕組みから、CSRFトークンを使った防御方法まで、セキュリティ学習を始めたばかりの方でも理解できるよう、丁寧に解説していきます。
CSRF攻撃とは何か?その仕組みを理解しよう
リンドくんそもそもCSRF攻撃って、どういう仕組みで起こるんですか?
たなべ良い質問だね。まず、Webサイトにログインすると、ブラウザにクッキー(Cookie)という情報が保存されるよね。
CSRF攻撃は、このクッキーが自動的に送信される仕組みを悪用するんだ。
CSRF攻撃の基本的な流れ
CSRF攻撃がどのように行われるのか、具体的な流れを見ていきましょう。
- ユーザーがWebサイトにログイン - 例えば、銀行のサイトやSNSにログインします
- ログイン状態が維持される - ブラウザにセッションクッキーが保存されます
- 攻撃者が用意した罠サイトにアクセス - ユーザーが悪意あるサイトを訪問します
- 罠サイトから正規サイトへリクエスト送信 - 画像タグやフォームなどを使って、隠れたリクエストが送られます
- ブラウザが自動的にクッキーを送信 - ログイン状態のクッキーが一緒に送られてしまいます
- 正規サイトが正当なリクエストと判断 - サーバーは正規のユーザーからのリクエストだと誤認します
- 意図しない操作が実行される - パスワード変更や送金などが勝手に行われます
具体的な攻撃例
例えば、以下のようなHTMLコードが悪意あるサイトに仕込まれていたとします。
<!-- 攻撃者が用意した罠サイトのコード -->
<img src="https://bank-____.com/transfer?to=attacker&amount=100000"
style="display:none;">このコードは、画面には何も表示されませんが、ページを開いた瞬間に銀行サイトへ送金リクエストを送ってしまいます。もしユーザーが銀行サイトにログインしたままこのページを見ると、知らないうちに送金が実行される可能性があるのです。
なぜCSRF攻撃が成功してしまうのか
CSRF攻撃が成功してしまう主な理由は、以下の2点です。
- ブラウザが自動的にクッキーを送信する - どのサイトからのリクエストでも、対象サイトのクッキーは自動で送られます
- サーバー側でリクエストの正当性を確認していない - 正規のユーザーからのリクエストか、罠サイト経由のリクエストかを区別できません
これらの特性を悪用することで、攻撃者はユーザーの権限で様々な操作を実行できてしまうのです。
CSRFトークンによる防御の仕組み
リンドくんじゃあ、どうやって防げばいいんですか?
たなべそこで登場するのがCSRFトークンなんだ。
これは予測不可能なランダムな文字列を使って、リクエストが本当に正規のページから送られたものかを確認する仕組みなんだよ。
CSRFトークンとは
CSRFトークンは、サーバーが生成する予測不可能なランダムな文字列です。
この文字列を使って、リクエストが正規のフォームから送信されたものかを確認します。
トークンの特徴は以下の通りです。
- ユーザーごと、またはセッションごとに異なる値が生成されます
- 予測が極めて困難なランダムな文字列です
- 有効期限を設定できます
- サーバー側で検証されます
CSRFトークンの動作フロー
CSRFトークンを使った防御の流れを見ていきましょう。
-
ユーザーがフォームページにアクセス
- サーバーがCSRFトークンを生成します
- トークンをセッションに保存します
- トークンをHTMLフォームに埋め込みます
-
ユーザーがフォームを送信
- フォームデータと一緒にトークンも送信されます
-
サーバーがトークンを検証
- 送信されたトークンとセッションのトークンを比較します
- 一致すれば正規のリクエストと判断します
- 不一致ならリクエストを拒否します
実装例 - HTMLフォームへのトークン埋め込み
実際のコード例を見てみましょう。以下はPHPでの実装例です。
<?php
// セッション開始
session_start();
// CSRFトークンの生成(まだ生成されていない場合)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
?>
<!DOCTYPE html>
<html>
<head>
<title>パスワード変更</title>
</head>
<body>
<h1>パスワード変更フォーム</h1>
<form method="POST" action="change_password.php">
<!-- CSRFトークンを隠しフィールドとして埋め込む -->
<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>">
<label>新しいパスワード:</label>
<input type="password" name="new_password" required>
<button type="submit">変更する</button>
</form>
</body>
</html>実装例 - サーバー側でのトークン検証
次に、送信されたトークンを検証するコードです。
<?php
// change_password.php - パスワード変更処理
session_start();
// POSTリクエストかチェック
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('不正なアクセスです');
}
// CSRFトークンの検証
if (!isset($_POST['csrf_token']) ||
!isset($_SESSION['csrf_token']) ||
$_POST['csrf_token'] !== $_SESSION['csrf_token']) {
// トークンが一致しない場合は拒否
die('CSRFトークンが無効です');
}
// トークン検証が成功したら、パスワード変更処理を実行
$new_password = $_POST['new_password'];
// パスワード変更のロジック
// (実際のアプリケーションでは適切なバリデーションとハッシュ化が必要)
// update_user_password($user_id, $new_password);
echo 'パスワードを変更しました';
// 使用済みトークンを無効化(ワンタイムトークンの場合)
unset($_SESSION['csrf_token']);
?>なぜこれで攻撃を防げるのか
CSRFトークンによって攻撃が防げる理由を整理しましょう。
- 攻撃者はトークンの値を知ることができない - 正規のページにアクセスしないとトークンは取得できません
- 罠サイトからのリクエストにはトークンが含まれない - 攻撃者がトークンを予測することは不可能です
- サーバー側で厳密に検証される - トークンが一致しない場合、リクエストは確実に拒否されます
このように、CSRFトークンは正規のフォームから送信されたリクエストである証明として機能するのです。
より安全なCSRFトークンの実装パターン
リンドくん基本的な仕組みはわかりました!でも、もっと安全にする方法ってありますか?
たなべいい質問だね!実は、CSRFトークンにもいくつかの実装パターンがあって、それぞれに特徴があるんだ。
状況に応じて使い分けることで、より強固なセキュリティを実現できるよ。
パターン① ワンタイムトークン(使い捨てトークン)
ワンタイムトークンは、一度使用したら無効になるトークンです。
メリット
- セキュリティレベルが最も高い
- トークンの再利用による攻撃を防げる
デメリット
- ブラウザの「戻る」ボタンで問題が起きやすい
- フォームの二重送信対策が別途必要
<?php
// ワンタイムトークンの実装例
session_start();
// トークン検証後、すぐに新しいトークンを生成
if (verify_csrf_token($_POST['csrf_token'])) {
// 処理実行
process_form_data();
// 使用済みトークンを削除し、新しいトークンを生成
unset($_SESSION['csrf_token']);
$_SESSION['csrf_token'] = generate_new_token();
}
?>パターン② セッション固定トークン
セッション中は同じトークンを使い続けるパターンです。
メリット
- 実装がシンプル
- ブラウザの「戻る」ボタンでも問題なし
- ユーザビリティが良好
デメリット
- トークンの有効期間が長くなる
- セッション中は同じトークンが使われる
<?php
// セッション固定トークンの実装例
session_start();
// セッション開始時に一度だけ生成
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 検証時は削除せずに再利用
if ($_POST['csrf_token'] === $_SESSION['csrf_token']) {
process_form_data();
// トークンはそのまま保持
}
?>パターン③ ダブルサブミットクッキー
トークンをクッキーとフォームの両方に埋め込むパターンです。
メリット
- サーバー側でセッション管理が不要
- スケーラビリティが高い(複数サーバーでも動作しやすい)
デメリット
- クッキーの設定を適切に行う必要がある
- XSS脆弱性があると突破される可能性がある
<?php
// ダブルサブミットクッキーの実装例
// トークン生成とクッキー設定
$token = bin2hex(random_bytes(32));
setcookie('csrf_token', $token, [
'httponly' => true,
'secure' => true,
'samesite' => 'Strict'
]);
?>
<!-- HTMLフォーム -->
<form method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $token; ?>">
<!-- フォームの内容 -->
</form>
<?php
// 検証処理
if ($_POST['csrf_token'] === $_COOKIE['csrf_token']) {
// トークンが一致すれば正規のリクエスト
process_form_data();
}
?>有効期限の設定
トークンに有効期限を設けることで、セキュリティをさらに向上させることができます。
<?php
// 有効期限付きトークンの実装例
session_start();
// トークンと有効期限を一緒に保存
if (!isset($_SESSION['csrf_token']) ||
!isset($_SESSION['csrf_token_time']) ||
time() - $_SESSION['csrf_token_time'] > 3600) { // 1時間
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$_SESSION['csrf_token_time'] = time();
}
// 検証時に有効期限もチェック
function verify_csrf_token($submitted_token) {
if (!isset($_SESSION['csrf_token']) ||
!isset($_SESSION['csrf_token_time'])) {
return false;
}
// 有効期限チェック(1時間)
if (time() - $_SESSION['csrf_token_time'] > 3600) {
return false;
}
// トークン一致チェック
return $submitted_token === $_SESSION['csrf_token'];
}
?>フレームワークでのCSRF対策
リンドくん実際のプロジェクトでは、いつも一から実装するんですか?
たなべいや、実は多くのWebフレームワークには標準でCSRF対策機能が組み込まれているんだ。
それを使えば、比較的簡単に安全な実装ができるよ。
Laravel(PHP)での実装
Laravelでは、CSRFトークンが自動的に生成されます。
<!-- Laravelのbladeテンプレート -->
<form method="POST" action="/password/change">
@csrf <!-- これだけでCSRFトークンが埋め込まれる -->
<input type="password" name="new_password">
<button type="submit">変更する</button>
</form>Laravelは自動的にトークンを検証し、一致しない場合は419 Page Expiredエラーを返します。
Django(Python)での実装
Djangoでも同様に簡単に実装できます。
<!-- Djangoテンプレート -->
<form method="post">
{% csrf_token %} <!-- CSRFトークンの埋め込み -->
<input type="password" name="new_password">
<button type="submit">変更する</button>
</form>Express(Node.js)での実装
Expressではcsurfミドルウェアを使います。
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
// CSRFミドルウェアの設定
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
// フォーム表示
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// フォーム送信処理
app.post('/process', csrfProtection, (req, res) => {
// トークンが自動的に検証される
res.send('成功しました');
});フレームワークを使うメリット
フレームワークのCSRF対策機能を使うことで、以下のメリットがあります。
- 実装ミスを防げる - 検証された安全な実装が提供されます
- メンテナンスが楽 - セキュリティアップデートが自動的に適用されます
- 開発速度が向上 - 一から実装する必要がありません
- 標準化 - チーム開発で統一された実装ができます
初心者の方は、まずフレームワークの機能を使って実装することをおすすめします。その上で、仕組みをしっかり理解することが大切です。
CSRF対策で気をつけるべきポイント
リンドくんCSRFトークンを実装すれば完璧なんですか?
たなべ残念ながら、それだけでは不十分なんだ。CSRFトークンと組み合わせて使うべきセキュリティ対策がいくつかあるんだよ。
SameSite Cookie属性の設定
クッキーにSameSite属性を設定することで、CSRF攻撃のリスクを大幅に減らせます。
<?php
// SameSite属性を設定したクッキー
setcookie('session_id', $session_id, [
'httponly' => true, // JavaScriptからアクセス不可
'secure' => true, // HTTPS通信のみ
'samesite' => 'Strict' // 同じサイトからのリクエストのみ
]);
?>SameSiteには3つの値があります。
- Strict - 最も厳格。他サイトからのリクエストでは一切送信されません
- Lax - デフォルト推奨。リンククリックなど安全なナビゲーションでは送信されます
- None - 制限なし。
Secure属性との併用が必須です
Content Security Policy(CSP)の設定
CSPヘッダを設定することで、外部サイトからのリクエスト送信を制限できます。
<?php
// CSPヘッダの設定例
header("Content-Security-Policy: default-src 'self'; form-action 'self'");
?>Refererヘッダのチェック
リクエスト元のURLを確認することで、追加の防御層を設けられます。
<?php
// Refererチェックの例
function check_referer() {
if (!isset($_SERVER['HTTP_REFERER'])) {
return false;
}
$referer = parse_url($_SERVER['HTTP_REFERER']);
$host = $_SERVER['HTTP_HOST'];
return $referer['host'] === $host;
}
if (!check_referer()) {
die('不正なアクセスです');
}
?>ただし、Refererヘッダは補助的な対策として使用してください。ブラウザやプロキシによって削除される場合があるため、これだけに頼るのは危険です。
GETリクエストでの状態変更を避ける
重要な操作は必ずPOSTリクエストで行うようにしましょう。
<?php
// 悪い例:GETでの削除処理
// <a href="delete.php?id=123">削除</a>
// これは画像タグなどで簡単に攻撃できてしまいます
// 良い例:POSTでの削除処理
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('POSTリクエストが必要です');
}
// CSRFトークンの検証
verify_csrf_token($_POST['csrf_token']);
// 削除処理
delete_item($_POST['id']);
?>GETリクエストはべき等(何度実行しても結果が同じ)である必要があります。状態を変更する操作には、必ずPOST、PUT、DELETEなどのメソッドを使用しましょう。
まとめ
リンドくんCSRF攻撃の仕組みとトークンによる防御方法、よくわかりました!
たなべ素晴らしいね!最後に大事なポイントをまとめておこう。
セキュリティは一つの対策だけでは不十分で、多層防御が基本なんだ。これからWebアプリを作るときは、ぜひ意識してみてね。
この記事では、CSRF攻撃の仕組みと、CSRFトークンを使った防御方法について詳しく解説してきました。
重要なポイントを改めて整理しましょう。
- ログイン状態を悪用される攻撃である
- クッキーが自動送信される仕組みを利用している
- ユーザーの意図しない操作が実行されてしまう
- 送金やパスワード変更など重大な被害につながる可能性がある
- 予測不可能なランダム文字列をフォームに埋め込む
- サーバー側で厳密に検証する
- ワンタイムトークンやセッション固定など、状況に応じて実装パターンを選択する
- 有効期限を設定してセキュリティを強化する
- フレームワークの機能を積極的に活用する
- SameSite Cookie属性を適切に設定する
- GETリクエストで状態を変更しない
- CSP(Content Security Policy)も併用する
- 多層防御の考え方を持つ
セキュリティは、後から付け加えるものではなく、設計段階から組み込むべきものです。
CSRF対策は決して難しいものではありませんが、理解せずに実装すると、思わぬ脆弱性を生み出してしまう可能性があります。
今回学んだCSRF対策の知識を、ぜひあなたのプロジェクトに活かしてください。
最初は面倒に感じるかもしれませんが、ユーザーの大切なデータや資産を守るために、セキュリティ対策は欠かせません。
あなたの開発するアプリケーションが、安全で信頼されるものとなることを願っています。





