【Laravel】CSRF対策の仕組みとLaravelでの実装

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はPolicy・Gateを使った認可の仕組みを扱いました。

今回は、Webアプリのセキュリティで必ず押さえておきたいCSRF(クロスサイトリクエストフォージェリ)対策について解説します。

Laravelは標準でCSRF対策の仕組みを持っていますが、「なぜそれが必要なのか」を理解していないと、フォーム送信でエラーが出たときに正しく対処できません。

仕組みから丁寧に見ていきましょう。

CSRFとは?どんな攻撃なのか

CSRF攻撃の仕組み

CSRF(Cross-Site Request Forgery:クロスサイトリクエストフォージェリ)とは、ログイン中のユーザーに気づかれないまま、悪意のあるリクエストを送信させる攻撃です。

具体的な流れはこうです。

  1. ユーザーが正規のサイト(例:オンラインバンキング)にログインし、セッションCookieが発行される
  2. ユーザーが別タブで悪意のあるサイトを開く
  3. その悪意のあるサイトに、正規サイトの「送金処理」を実行するフォームが仕込まれている
  4. ユーザーが気づかずページを開いただけで、ブラウザは正規サイトのCookieを自動的に付けてリクエストを送ってしまう
  5. 正規サイト側は「ログイン中のユーザー本人からのリクエスト」と誤認し、送金処理が実行されてしまう

ポイントは、ブラウザはドメインが違っても同じCookieを自動的に送ってしまうという性質を悪用している点です。

ユーザー本人は何も操作していないのに、意図しない処理が実行されてしまうのがCSRFの怖いところです。

なぜこの攻撃が成立してしまうのか

サーバー側が「ログイン中のセッションCookieが送られてきた」ことだけを根拠にリクエストを信頼してしまうと、この攻撃は簡単に成立します。

つまり、リクエストの送信元が「本当に自分のサイトのフォームから送られたものか」を検証する仕組みがないと、CSRFを防げません。

この検証のために使われるのが、CSRFトークンという仕組みです。

CSRFトークンによる対策の仕組み

トークンで正規のリクエストだと証明する

CSRFトークンの考え方はシンプルです。

  1. サーバーがページを表示するときに、ランダムな文字列(トークン)を発行し、セッションに保存する
  2. 同じトークンをフォームの隠しフィールドにも埋め込む
  3. フォーム送信時、サーバーはリクエストに含まれるトークンと、セッションに保存したトークンが一致するかを確認する
  4. 一致すれば「正規のフォームから送信されたリクエスト」として処理を続行する

悪意のあるサイトは、このトークンの値を知る手段がありません(同一生成元ポリシーにより、他サイトから正規サイトのページ内容を読み取ることはできないため)。

そのため、トークンが一致しないリクエストは弾かれ、CSRF攻撃を防げるという仕組みです。

LaravelにおけるCSRF対策の実装

@csrfディレクティブの利用

Laravelでは、Bladeテンプレートで@csrfディレクティブを書くだけで、CSRFトークンを自動的に埋め込んでくれます。

<form method="POST" action="/posts">
    @csrf
    <input type="text" name="title">
    <button type="submit">投稿する</button>
</form>

@csrfは、実際には次のような隠しフィールドに展開されます。

<input type="hidden" name="_token" value="XXXXXXXXXXXXXXXXXXXXXXXX">

このトークンは、webミドルウェアグループに含まれるVerifyCsrfTokenミドルウェアによって自動的に検証されます。

routes/web.phpで定義したルートは、基本的にこのミドルウェアの対象になっています。

トークン不一致で発生する419エラー

CSRFトークンの検証に失敗すると、Laravelは419(Page Expired)エラーを返します。

これは、私が初めてLaravelアプリを触っていたときによく遭遇したエラーです。

原因の多くは次のようなケースでした。

  • フォームに@csrfを書き忘れている
  • セッションの有効期限が切れた状態で、開いたままのタブからフォーム送信した
  • Ajaxでリクエストを送る際に、トークンをヘッダーに含め忘れている

特に「タブを開いたまま長時間放置して、久しぶりにフォームを送信したら419になった」というのは、CSRFトークンの仕組みを理解していないと「なぜ?」となりやすい現象です。

セッションの有効期限が切れるとトークンも無効になるため、ユーザーには「セッションが切れました。もう一度お試しください」のような分かりやすいエラーメッセージを表示する配慮があると親切です。

つまずきやすいポイント:フォームの書き忘れとAjax送信

フォームに@csrfを書き忘れる

❌ Before:CSRFトークンを埋め込まないフォーム

<!-- @csrfを書き忘れている -->
<form method="POST" action="/posts">
    <input type="text" name="title">
    <button type="submit">投稿する</button>
</form>

このままフォームを送信すると、_tokenパラメータが存在しないため、VerifyCsrfTokenミドルウェアが検証に失敗し、419エラーになります。

✅ After:@csrfディレクティブを必ず入れる

<form method="POST" action="/posts">
    @csrf
    <input type="text" name="title">
    <button type="submit">投稿する</button>
</form>

POSTやPUT・DELETEなど、GET以外のメソッドでフォームを作るときは、@csrfを書く習慣を最初から身につけておくのが一番です。

Ajax(fetch/axios)でのCSRFトークン送信

Bladeのフォームではなく、JavaScriptからAjaxでリクエストを送る場合は、@csrfディレクティブが使えないため別の対応が必要です。

❌ Before:トークンを付けずにfetchでPOSTする

// トークンなしでリクエストしてしまい419エラーになる
fetch('/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ title: '新しい投稿' }),
});

✅ After:metaタグからトークンを取得し、ヘッダーに含める

まず、レイアウトの<head>にトークンをmetaタグとして埋め込みます。

<meta name="csrf-token" content="{{ csrf_token() }}">

そのうえで、JavaScript側でmetaタグからトークンを取得し、X-CSRF-TOKENヘッダーに付けてリクエストを送ります。

const token = document.querySelector('meta[name="csrf-token"]').content;

fetch('/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': token,
    },
    body: JSON.stringify({ title: '新しい投稿' }),
});

LaravelのVerifyCsrfTokenミドルウェアは、_tokenパラメータだけでなくX-CSRF-TOKENヘッダーもチェックしてくれるため、この方法で検証を通過できます。

axiosを使っている場合は、XSRF-TOKENというCookieを自動的に読み取り、X-XSRF-TOKENヘッダーに設定してくれる仕組みが用意されているため、さらに手軽に扱えます。

応用:CSRF検証を除外したいケース

Webhookなど外部サービスからのリクエスト

外部サービスからのWebhook通知など、フォームを介さずサーバー間で直接POSTされるエンドポイントでは、CSRFトークンを付けられないため、検証を除外する必要があります。

<?php
// app/Http/Middleware/VerifyCsrfToken.php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * CSRF検証を除外するURI
     */
    protected $except = [
        'webhook/stripe',
    ];
}

ただし、除外したエンドポイントはCSRF対策の対象外になるため、代わりに署名検証(Webhookの署名をチェックする仕組み)など、別の方法でリクエストの正当性を確認する必要があります。

「CSRFを除外する=何もチェックしなくていい」ではない、という点は必ず意識しておきましょう。

APIルート(routes/api.php)は、そもそもトークン認証(前回紹介したSanctumなど)を使うことが多く、apiミドルウェアグループにはCSRF検証が含まれていないのが標準です。

まとめ

この記事のポイント

  • CSRFは、ログイン中のユーザーに気づかれず悪意のあるリクエストを送信させる攻撃
  • Laravelは@csrfディレクティブとトークン検証でCSRF対策を標準搭載している
  • トークン不一致は419エラーとして現れ、原因の多くは@csrfの書き忘れやセッション切れ
  • Ajax送信ではmetaタグからトークンを取得し、X-CSRF-TOKENヘッダーに含める
  • Webhookなど除外が必要な場合は、代わりに署名検証などで正当性を担保する

次に読むべき記事

CSRF対策の次は、Webセキュリティのもう一つの基本であるXSS対策を見ていきましょう。

→ 次の記事:XSS対策:Bladeのエスケープ処理を理解する

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