【入門編】 クロスサイトリクエストフォージェリ(CSRF)対策とトークン検証 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!Webアプリケーション開発やセキュリティの勉強、日々お疲れ様です。
新人のIT担当者さんや、これからセキュリティの仕組みをしっかり学んでいきたいという開発者の方に向けて、今回はWebの代表的な攻撃の一つである「CSRF(クロスサイトリクエストフォージェリ)」と、そのガチガチな防御策について、身近な防犯の例えを交えながら優しく紐解いていきたいと思います。

セキュリティの専門用語って、最初は呪文みたいに難しく聞こえますよね。でも、一つひとつの仕組みは私たちの日常生活にある防犯対策とまったく同じです。一歩ずつ、リラックスして学んでいきましょう!

—

1. 泥棒の新手口?CSRF(クロスサイトリクエストフォージェリ)ってなぁに?

まずは、攻撃者がどんな手口を使って私たちを罠にはめるのか、イメージしてみましょう。

身近な例え:合鍵泥棒と「勝手に注文書」の罠

想像してみてください。あなたは信頼している通販サイトにログインしっぱなしで、自分の部屋(マイページ)でくつろいでいます。この状態は、いわば「家の鍵を開けたままリビングのソファでリラックスしている状態」です。

そこに、悪意あるA君がやってきて、あなたのポストにこっそり「高級レストランの豪華ディナーをこの家に10人前届けてください(代金は住人払い)」という注文書を投函しました。
もしあなたが、その罠のサイト(A君が作った悪意あるホームページなど)を知らずに踏んでしまったとき、裏側でブラウザが勝手に「さっきの通販サイトへ注文書を送信する」という動作をしてしまったらどうでしょう?

通販サイト側からすると、「おっ、ログイン中のあなた自身がこの注文ボタンを押したんだな!」と勘違いして、注文を処理してしまいますよね。これがCSRF(クロスサイトリクエストフォージェリ)の正体です。
攻撃者はあなたのパスワードを盗むわけではありません。「あなたがログインしているという信頼関係を悪用して、あなたの代わりに勝手に裏でボタンを押させる」という、なんとも巧妙な代理攻撃なのです。

—

2. 破られないための「多層防御」:3つの合言葉

じゃあ、この厄介な泥棒を防ぐにはどうすればいいでしょうか?
セキュリティの基本は「多層防御(たそうぼうぎょ)」です。一つの鍵だけでなく、二重、三重の鍵をかけることで、万が一の突破を防ぎます。今回は次の3つの対策をセットで実装する方法を見ていきましょう。

1. CSRFトークン(本人しか知らない合言葉)
2. カスタムヘッダーによる検証(API通信の身分証確認)
3. SameSite属性のCookie(ブラウザの玄関での水際対策)

それぞれの意味を、優しく見ていきましょう!

—

3. 実装を見てみよう!今日から使える防御コード

ここからは、実際にWebアプリを作る際にどうやってこの3つを実装するのか、具体的なコード例と一緒に見ていきます。難しく考えず、「こういう仕組みで守るんだな」と雰囲気を掴んでみてくださいね。

対策①:ステートフルなCSRFトークン

「今から送るリクエストは、本人がちゃんと正規の画面から送ったものです」という証明のために、サーバー側だけが知っている使い捨ての「合言葉(トークン)」をフォームに忍ばせておきます。

PHPを使った簡単なサンプルを見てみましょう。

<?php
// セッションを開始します
session_start();

// まだセッションにCSRFトークンがなければ、ランダムな文字列(合言葉)を生成して保存します
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // 32バイトの安全なランダム文字列
}
?>

<!-- フォームの描画 -->
<form action="update_profile.php" method="POST">
    <!-- ユーザーには見えない隠しフィールド(hidden)に合言葉を仕込む -->
    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8'); ?>">
    
    <label>新しいメールアドレス:</label>
    <input type="email" name="email">
    
    <button type="submit">変更する</button>
</form>

そして、データを受け取る側の update_profile.php では、次のようにチェックを行います。

<?php
session_start();

// 送られてきたPOSTデータの中身と、セッションに保存した合言葉が一致するか厳密に比較する
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    // 合言葉が一致しない場合は、不正なアクセス(CSRF攻撃)とみなして処理を中断!
    http_response_code(403);
    die('セキュリティエラー: 不正なリクエストが検知されました。');
}

// 一致していれば安全なので、無事に処理を続行します
// 処理が終わったら、合言葉は使い捨てるため破棄(再生成)するのがベストプラクティスです
unset($_SESSION['csrf_token']);
echo 'メールアドレスが正常に変更されました!';
?>

対策②:カスタムヘッダーによる検証(APIを守る)

最近のモダンなWebアプリ(ReactやVue.jsなどを使ったSPA)では、画面の裏側でJavaScript(fetch APIなど)を使って非同期通信を行うことがよくありますよね。
この場合、ただのHTMLフォームではなく、ブラウザの特別な機能を使って「カスタムヘッダー(身分証)」をリクエストに添付することで、さらにセキュリティを強固にできます。

悪意ある外部のホームページから、勝手にあなたのサイトへリクエストを送ろうとしても、ブラウザのセキュリティ制限(CORS)があるため、勝手にこのカスタムヘッダーを付与してリクエストを送ることはできません。つまり、「カスタムヘッダーが付いていないリクエストは、よそ者からの不審なアクセスだ!」と弾くことができるのです。

JavaScriptでの送信例:

// フロントエンドからの非同期リクエスト送信例
fetch('/api/update-status', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest' // ← これがカスタムヘッダーの身分証!
    },
    body: JSON.stringify({ status: 'active' })
})
.then(response => response.json())
.then(data => console.log(data));

サーバー側(受け取り側)でのチェック:

<?php
// リクエストヘッダーに特定の身分証(X-Requested-With)が含まれているか確認する
$headers = apache_request_headers(); // Apacheの場合の例

if (!isset($headers['X-Requested-With']) || $headers['X-Requested-With'] !== 'XMLHttpRequest') {
    http_response_code(403);
    die('不正なAPIリクエストです。');
}

// 正常なリクエストの処理へ...
?>

対策③:SameSite属性のCookie併用(ブラウザの玄関での水際対策)

最後の砦は、セッションIDなどを保存するCookieに設定する SameSite属性 です。
これは、「他のウェブサイトから飛んできたときに、このCookie(鍵)を自動で一緒に持っていくかどうか」をブラウザにお願いする設定になります。

サーバーからCookieを発行する際、次のように設定します。

<?php
// PHPでセッションCookieを発行する際の設定例(PHP 7.3以降)
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,       // HTTPS通信のときのみCookieを送信する
    'httponly' => true,     // JavaScriptからCookieを盗まれないようにする
    'samesite' => 'Lax'     // ★ ここがポイント!
]);
session_start();
?>

この samesite には主に 3 つの設定値があります。

  • Strict: 完全に厳格。外部サイトからあなたのサイトへのリンクをクリックして訪れた場合ですら、Cookieを送信しません。セキュリティは最強ですが、外部サイトからの自然な流入でもログイン状態が切れたように見えてしまうため、利便性が少し下がります。
  • Lax: おすすめのバランス設定。通常のリンク経由(例:SNSのリンクを踏んで飛んできた等)ではCookieを送りますが、CSRFの温床になりやすい裏側からの自動送信(POSTリクエストや画像の読み込みなど)のときはCookieを送信しません。 一般的なWebサイトであれば、まずはこの Lax を設定しておけば安心です。
  • None: 外部サイトからのリクエストでも無条件にCookieを送信します(クロスドメインでどうしてもCookieが必要な場合に使いますが、必ず secure (HTTPS) とセットにする必要があります)。

—

4. まとめ:今日からできる一歩

いかがでしたでしょうか?
「CSRF」という言葉は難しく聞こえますが、要するに「ログインしているユーザーになりすまして、知らないうちに裏でボタンを押させる攻撃」のことでしたね。

これを防ぐためには、
1. CSRFトークンという「本人しか知らない合言葉」を仕込んでチェックする。
2. カスタムヘッダーを使って、外部からの不正なAPI呼び出しをシャットアウトする。
3. Cookieに SameSite=Lax を設定して、ブラウザの玄関口で不審な代理リクエストを水際で食い止める。

この3つの多層防御を組み合わせることで、あなたの作ったWebアプリケーションはぐっと安全になります。
セキュリティの対策に「これで完璧」というゴールはありませんが、こうした基本の仕組みを一つひとつ丁寧に実装していくことが、何よりの強固な盾になります。

一歩ずつ、安全で楽しいWeb開発のスキルを一緒に高めていきましょう!

コメント

タイトルとURLをコピーしました