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

こんにちは!Webアプリケーション開発の世界へようこそ。
セキュリティの世界に足を踏み入れたばかりの頃は、CSRFやらSameSiteやら、なんだか呪文のような難しい言葉がたくさん出てきて、頭がクラクラしてしまいますよね。

でも、安心してください。セキュリティ対策の基本は、私たちが普段暮らしている現実世界の「防犯」と同じです。今回は、家の鍵や泥棒のテクニックに例えながら、Webアプリの大きな落とし穴である「CSRF(クロスサイトリクエストフォージェリ)」と、その強力なガードマンである「SameSite属性」について、一緒に優しく紐解いていきましょう!

—

1. 家の鍵に例える「CSRF(クロスサイトリクエストフォージェリ)」の正体

まずは、攻撃者がどのようにしてあなたのWebアプリを狙うのか、その仕組みを「合鍵泥棒」に例えて考えてみましょう。

あなたが自宅(Webアプリ)のドアに、頑丈な鍵(ログインセッション)をかけたとします。
この鍵は非常に優秀で、一度ドアを開けて家に入れば(ログインすれば)、しばらくの間は鍵を出さなくても、ノブを回すだけで家の中に出入りできますよね。これがWebアプリにおける「ステートフルな認証(Cookieを使ったセッション維持)」の状態です。

さて、ここに泥棒(攻撃者)がやってきました。
泥棒は、あなたから本物の「合鍵(Cookie)」を盗み出すことはできません。しかし、次のような悪知恵を思いつきます。

1. 泥棒は、どこかの怪しい遊園地(悪意ある外部サイト)に、一見するとただの「プレゼント応募ボタン」を置きます。
2. そのボタンの裏側には、「このボタンを押すと、あなたの家の金庫から全財産を泥棒の口座に振り込む」という隠しコマンド(不正なリクエスト)が仕掛けられています。
3. あなたがうっかりその遊園地へ遊びに行き、何も知らずに「プレゼント応募ボタン」を押してしまいました。

ここで何が起きるでしょうか?
ブラウザは、「おっ、主人がこのボタンを押したんだな! じゃあ、いつも通りこの家の合鍵(Cookie)を添えて、金庫の振込指令を出してあげよう!」と、自動的に家(Webアプリ)へリクエストを送ってしまいます。

これが CSRF(クロスサイトリクエストフォージェリ:十字架型リクエスト偽造) です。
ユーザーが意図しないうちに、ブラウザが勝手に「正当なふりをした不正なリクエスト」を送信してしまうのが、この攻撃の恐ろしいところなんです。

—

2. 泥棒を完封する!「CSRFトークン」という合言葉

この泥棒の侵入を防ぐために考え出された最初の防衛策が、「CSRFトークン」という仕組みです。

これを現実世界の防犯に例えるなら、「家の中に入る時に毎回変わる、家族だけの秘密の合言葉」です。

Webアプリ側は、ユーザーがフォームを表示するたびに、推測不可能なランダムな文字列(トークン)を発行し、こっそりHTMLのフォームに仕込んでおきます。

実際のコードを見てみましょう(PHPの例)

例えば、パスワードを変更する画面の裏側では、以下のようにトークンが埋め込まれています。

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

// まだトークンがなければ、推測されにくいランダムな文字列を生成してセッションに保存します
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>

<!-- パスワード変更フォーム -->
<form action="/update_password.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="password" name="new_password">
    <button type="submit">パスワードを変更する</button>
</form>

そして、ユーザーが「変更する」ボタンを押したとき、サーバー側では次のようなチェックを行います。

<?php
session_start();

// 送信されてきたトークンと、セッションに保存されているトークンが一致するか厳密に比較します
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    // 一致しない場合は、外部からの不正なリクエスト(CSRF攻撃)とみなして処理を拒否!
    header('HTTP/1.1 403 Forbidden');
    die('不正なリクエストが検知されました。');
}

// チェックを通過した場合のみ、安全にパスワード変更処理を実行します
// ... (データベースの更新処理など) ...
?>

外部の怪しいサイト(泥棒)は、あなたが今どんな「秘密の合言葉」を持っているのかを知ることができません。そのため、ボタンを偽装してリクエストを送っても、合言葉が不一致となり、サーバーは「おっと、怪しいな」と門前払いできるわけですね。

—

3. さらに強力な門番! Cookieの「SameSite属性」

CSRFトークンは非常に効果的ですが、「すべてのフォームやAPIリクエストに毎回トークンを仕込むのは、開発もうっかりミスをしそうだし大変だな……」と感じる方もいるでしょう。

そこで登場するのが、ブラウザのCookieに設定できる SameSite属性 です。これは、現代のWebセキュリティにおいて欠かせない強力な門番となっています。

現実世界に例えるなら、「普段の買い物(同一サイト)では自由に通れるけれど、よそ様のイベント会場(外部サイト)から来た使い走りには、合鍵の持ち出しを一切禁止する玄関の特殊なドアチェーン」のようなものです。

Cookieに SameSite属性を設定することで、ブラウザに対して「このCookieを、外部サイトからのリクエストの際に送信してよいか」を指示できます。設定できる値は主に以下の2つ(実質的な標準はLax)です。

① SameSite=Strict (非常に厳しい設定)

外部サイトからあなたのサイトへ移動してきた場合、どのようなリクエストであっても、一切Cookieを送信しません。

  • メリット: CSRFに対して最強の防御力を誇ります。
  • デメリット: 例えば、外部のニュースサイトにあるあなたのサイトへのリンクをクリックしてログイン後のページに飛んだときすら、Cookieが送られないため「未ログイン状態」に見えてしまいます。ユーザービリティ(使いやすさ)が少し下がります。

② SameSite=Lax (現代のデフォルト、程よい設定)

通常のリンクをクリックしてページを移動する場合(例: GETリクエスト)はCookieを送りますが、外部サイトからのフォーム送信や、画像・スクリプトの読み込み(例: POSTリクエストや裏側の通信)ではCookieの送信をブロックします。

  • メリット: ユーザーが外部からリンクを踏んでやってきた時の利便性を保ちつつ、CSRFの主な標的である「勝手に裏側でデータを送らジグ(POST等)」を防げます。
  • 注意点: 攻撃者が GET メソッドでデータを書き換えられるような脆弱な設計(本来はNGですが)にしている場合は、すり抜けられるリスクが残ります。

—

4. 実務でどう設定する? Cookie付与のサンプル

それでは、実際にサーバーからブラウザへCookieを渡す際の設定を見てみましょう。PHPでのセッションCookie設定を例にします。

<?php
// PHP 7.3以降で利用可能な、安全なCookieパラメータの設定
// セッションを開始する前に設定する必要があります
session_set_cookie_params([
    'lifetime' => 0,                      // ブラウザを閉じたらセッション切断
    'path'     => '/',                      // サイト全体の共通パス
    'domain'   => 'example.com',            // 対象のドメイン
    'secure'   => true,                     // 【重要】暗号化された通信(HTTPS)の時のみCookieを送る
    'httponly' => true,                     // 【重要】JavaScriptからCookieを盗み見られないようにする(XSS対策)
    'samesite' => 'Lax'                     // 【重要】外部サイトからの不正なPOSTリクエストを防ぐ門番
]);

session_start();
// ... 以降の処理 ...
?>

このように、'samesite' => 'Lax'(または厳重を期すなら 'Strict')を指定しておくことで、開発者がすべてのフォームに細かくCSRFトークンを仕込み忘れたとしても、ブラウザのレイヤーで強力に不正なリクエストをブロックしてくれるようになります。

—

5. まとめ:油断大敵、多層防御で守りを固めよう

いかがでしたでしょうか? 今回のポイントをギュッとまとめておきます。

1. CSRFとは?:ログイン中のユーザーのブラウザを悪用し、外部の悪意あるサイトから勝手にリクエストを送信させる攻撃。
2. CSRFトークン:サーバーとブラウザの間で「秘密の合言葉」を共有し、一致しないリクエストを弾く伝統的かつ確実な手法。
3. SameSite属性(Strict / Lax):ブラウザ自身に「外部からの怪しいリクエストにはCookieを添えないで!」とお願いする、現代の強力な門番。

ただし、セキュリティの世界に「これさえやっておけば100%安全」という魔法の杖はありません。
SameSite属性は非常に優秀ですが、古いブラウザではサポートされていなかったり、前述の通り GET リクエストを通す Lax の特性を突いた攻撃手法も研究されています。

だからこそ、「SameSite属性でブラウザの隙を埋めつつ、重要な操作(データの更新や削除など)には必ずCSRFトークンを併用する」という、複数の防壁を組み合わせた「多層防御(デプス・ディフェンス)」の意識が、実務では何よりも大切になってきます。

一歩ずつ、安全なコードの書き方を学んで、信頼されるエンジニアを目指していきましょう!

コメント

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