【入門編】 Cross-Site Request Forgery (CSRF) トークンの実装と検証 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!Webアプリケーション開発の世界へようこそ。
新人のIT担当者さんや、「セキュリティってなんだか難しそう……」と少し不安を感じている一般開発者の皆さん、日々の開発やお仕事本当にお疲れ様です!

今回は、Webセキュリティの避けて通れない重要テーマである「CSRF(クロスサイト・リクエスト・フォージェリ)」と、その対策の要である「CSRFトークン」と「SameSite属性」について、一緒に紐解いていきましょう。

小難しい専門用語が出てきても安心してくださいね。身近な「お家の防犯」に例えながら、一歩ずつ優しく解説していきます!

—

1. そもそもCSRF(クロスサイト・リクエスト・フォージェリ)ってなに?

いきなりカタカナの長い名前が出てきましたが、日本語に訳すと「十字架型サイト間リクエスト偽造」……ではなく、要するに「あなたのブラウザを悪者(攻撃者)が操り、知らないうちに勝手に裏でお買い物やパスワード変更をさせちゃう攻撃」のことです。

お家に例えてみましょう 🏠

想像してみてください。あなたは今、近所の信頼できる「お気に入りのショッピングサイト」にログインした状態でお買い物をしています。これは、いわば「合鍵を持って家に入り、リビングでくつろいでいる状態」です。

ここに、悪意ある泥棒(攻撃者)がやってきて、あなたにこう囁きます。
「ねえねえ、こちらの面白いチラシを見てよ!」

あなたがうっかりその泥棒が用意した罠のリンク(悪意ある外部サイト)をクリックしたとします。すると、そのサイトの裏側で、泥棒はあなたの代わりにこう叫びました。

「おい、さっきのショッピングサイトの口座から、全額俺の口座に送金しろ!」

あなたがログインした状態(=合鍵を挿したままの状態)のブラウザは、ショッピングサイトから見ると「ご本人様からの正しいお願いだ!」と勘違いしてしまいます。その結果、あなたの意思とは関係なく、勝手にお金が送金されてしまう……これがCSRFの恐ろしい手口です。

—

2. 泥棒を防ぐ最強のペア:「CSRFトークン」と「SameSite Cookie」

この恐ろしい泥棒の手口を防ぐためには、二重のガードをかける必要があります。それが今回主役となる「CSRFトークン」と、Cookieの設定である「SameSite属性」です。

防御の要①:使い捨ての合言葉「CSRFトークン」

先ほどのお家の例えに戻りましょう。あなたがショッピングサイトでお金を動かすとき、「本人確認の合言葉(ワンタイムパスワードのようなもの)」があらかじめ発行されていたらどうでしょうか?

ショッピングサイトの裏側は、あなただけにこっそり「今回のリクエスト専用の秘密の合言葉(CSRFトークン)」を渡しています。あなたがボタンを押すとき、ブラウザはこの合言葉を一緒に添えて送らなければ、サイト側は処理を受け付けません。

泥棒は外部のサイトからあなたを操ることはできても、あなたのブラウザの裏側に隠された「秘密の合言葉」を盗み見ることはできません。だから、泥棒がいくら送金命令を出そうとしても、合言葉がないためショッピングサイトに「偽物だな!」と見破られて弾き返されるわけですね。

防御の要②:そもそも泥棒を家に入れない「SameSite Cookie」

もう一つの強力な助っ人が、Cookieに設定する SameSite という属性です。
これは、「よその怪しい家(外部サイト)から、このお家の合鍵(Cookie)を使って勝手に入ってくるのを禁止する」というおまわりさんのような役割を持っています。

この設定をしておくと、あなたがどれだけうっかり怪しいリンクを踏んだとしても、ブラウザが「おや、これはよそから送られたリクエストだな。合鍵を使うのは禁止されているから、Cookieは渡さないぞ!」と自動的にブロックしてくれます。

—

3. 実装を見てみよう:ステートフルなCSRFトークンの仕組み

それでは、実際のWebアプリケーション(今回はPHPを例にします)で、どのようにCSRFトークンが生成され、検証されるのかを見ていきましょう。

① トークンの生成とセッションへの保存

ユーザーがフォームのページを開いた際、サーバー側でランダムな文字列(トークン)を生成し、サーバーのセッション(金庫)に保存すると同時に、フォームの隠しフィールド(input type="hidden")に仕込みます。

<?php
// セッションを開始します(サーバー側に安全な保管場所を作るイメージです)
session_start();

// まだトークンがなければ、予測不可能な安全なランダム文字列を生成します
if (empty($_SESSION['csrf_token'])) {
    // random_bytesで安全な乱数を生成し、16進数に変換します
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf_token'];
?>

<!-- ユーザーに表示する送金フォームの例 -->
<form action="transfer.php" method="POST">
    <!-- ユーザーには見えない隠しフィールドに「合言葉(トークン)」を仕込みます -->
    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
    
    <label>送金金額:</label>
    <input type="number" name="amount" value="1000">
    <button type="submit">送金する</button>
</form>

② サーバー側での検証処理

ユーザーが「送金する」ボタンを押すと、フォームに入力されたデータと一緒に、隠し持っていた合言葉(csrf_token)がサーバーに飛んできます。サーバーは、自分がセッションに保存していた合言葉と一致するかを厳しくチェックします。

<?php
session_start();

// リクエストがPOSTメソッドで送信されたか確認します
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    
    // ① 送信されてきたトークンと、サーバーのセッションに保存されていたトークンを比較します
    // hash_equalsを使うことで、タイミング攻撃と呼ばれるセキュリティホールを防ぎます
    if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        // トークンが一致しない、または存在しない場合は攻撃とみなして処理を中断します!
        http_response_code(403);
        die('エラー: 不正なリクエストが検出されました(CSRFエラー)。');
    }

    // ② 検証をクリアした場合のみ、本来の処理(送金など)を実行します
    $amount = $_POST['amount'];
    // TODO: ここに実際の送金処理のコードを書きます
    
    // セキュリティのため、一度使ったトークンは破棄して新しいものに再生成するのがベストプラクティスです
    unset($_SESSION['csrf_token']);
    
    echo "送金が正常に完了しました!";
}
?>

—

4. SameSite属性の設定でおまわりさんを常駐させる

トークンによる二重チェックと合わせて、Cookieを発行する際には必ず SameSite 属性を設定しましょう。インフラやフレームワークの設定、またはPHPの setcookie 関数などで以下のように指定します。

<?php
// セッションCookieを発行する際のセキュアな設定例
// SameSite=Lax または SameSite=Strict を指定します
session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'domain'   => 'example.com',
    'secure'   => true,       // HTTPS通信でのみCookieを送信する
    'httponly' => true,       // JavaScriptからのアクセスを禁止してXSS対策にする
    'samesite' => 'Lax'       // 外部サイトからの意図しないCookie送信をブロックする
]);
session_start();
?>

SameSiteの値の選び方

  • Strict: 最も厳格です。別のサイトからのリンク踏みなどでもCookieが送信されなくなります(他のサイトから自サイトへ遷移した直後に未ログイン状態に見えることがあるため、マイページや管理画面などでよく使われます)。
  • Lax: 少し柔軟です。通常のリンク踏み(例:SNSのリンクから自サイトへジャンプする等)ではCookieが送られますが、悪意あるサイトからの裏側からのリクエスト(POSTや画像・スクリプトの読み込み)ではCookieがブロックされます。一般的なWebサイトのデフォルトとして非常にバランスが良い設定です。

—

5. まとめ:一歩ずつ、安全なWebの世界を作っていきましょう!

いかがでしたでしょうか?

  • CSRFは、ユーザーのログイン状態(合鍵)を悪用して勝手に操作しちゃう攻撃。
  • CSRFトークンは、本人だけが知っている「使い捨ての合言葉」で偽物をシャットアウトする仕組み。
  • SameSite属性は、そもそも怪しい外部サイトから合鍵を持ち出させないおまわりさん。

この2つをしっかりと組み合わせることで、あなたの作るWebアプリケーションはぐっと安全になります。最初は難しく感じるかもしれませんが、一つひとつの仕組みの「なぜ?」を紐解いていけば、決して恐れるものではありません。

ぜひ、日々の開発現場でも「ここにトークンはあるかな?」「Cookieの設定は安全かな?」と確認する習慣をつけてみてくださいね。一歩ずつ、安全で頼れるエンジニアへの階段を登っていきましょう!応援しています!

コメント

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