LaravelのCSRF対策とは?@csrfとサーバー側の確認を初心者向けに解説

Laravel

CSRFとは

CSRFは、Cross-Site Request Forgeryの略です。日本語では「クロスサイトリクエストフォージェリ」と呼ばれます。

CSRFは、ログイン中のユーザーに、本人が意図していないリクエストを送らせる攻撃です。

たとえば、あるユーザーがLaravelで作られたサイトにログインしているとします。そのサイトでは、プロフィール変更、メールアドレス変更、記事削除、退会などの操作ができます。

このとき、外部から同じ送信先へリクエストを送られると、サーバー側ではログイン中のユーザー本人の操作のように見えてしまう場合があります。

ブラウザは、対象のサイトへリクエストを送るときにCookieを自動で付けます。ログイン状態をCookieやセッションで管理している場合、サーバー側では「ログイン中のユーザーから来たリクエスト」と判断されることがあります。

CSRF対策では、「ログイン中のユーザーから来たリクエストか」だけではなく、「正しいリクエスト元から送られたか」「Laravelが発行したCSRFトークンを持っているか」を確認します。

LaravelではCSRF対策が用意されている

Laravelでは、CSRF対策が標準で用意されています。

Laravelの通常のWebルートは、webミドルウェアグループを通ります。このwebミドルウェアグループには、セッション管理やCSRF保護が含まれています。

Bladeでフォームを作る場合は、フォーム内に@csrfを書きます。

@csrfを書くと、LaravelがCSRFトークン用のhidden項目をフォーム内に出力します。

<input type="hidden" name="_token" value="ランダムなトークン">

フォーム送信時には、この_tokenも一緒にサーバーへ送信されます。

Laravel 13のCSRF保護の流れ

Laravel 13では、CSRF保護のミドルウェア名がPreventRequestForgeryになっています。

Laravel 12以前で使われていたVerifyCsrfTokenValidateCsrfTokenは、Laravel 13でも非推奨のエイリアスとして残っています。

Laravel 13のCSRF保護は、CSRFトークンだけを見る仕組みではありません。

Laravel 13では、まずブラウザが送るSec-Fetch-Siteヘッダーを使って、リクエスト元を確認します。

Sec-Fetch-Siteヘッダーによって同一オリジンからのリクエストだと確認できる場合、そのリクエストは許可されます。

リクエスト元の確認が通らない場合は、CSRFトークンの確認に進みます。

流れは次のようになります。

  1. Sec-Fetch-Siteヘッダーでリクエスト元を確認する
  2. 同一オリジンのリクエストだと確認できる場合は許可する
  3. リクエスト元の確認が通らない場合はCSRFトークンを確認する
  4. CSRFトークンも正しくない場合はリクエストを拒否する

Laravel 13では、@csrfがないリクエストがすべて必ず拒否される、という説明は正確ではありません。

ただし、LaravelのHTMLフォームでPOSTPUTPATCHDELETEを使う場合は、CSRFトークンを含める形で作ります。Bladeでは、そのために@csrfを書きます。

CSRF対策が必要な処理

CSRF対策が必要になるのは、主にデータを変更する処理です。

  • プロフィールを変更する
  • メールアドレスを変更する
  • パスワードを変更する
  • 記事を投稿する
  • 記事を編集する
  • 記事を削除する
  • 商品を購入する
  • 退会する

これらの処理では、ユーザー本人が正しい画面から送信したリクエストかどうかを確認する必要があります。

LaravelのBladeでフォームを作る場合は、変更系のフォームに@csrfを書きます。

手順1:Controllerを作る

プロフィール編集フォームを例にして、LaravelでCSRF対策つきのフォームを作ります。

まず、Controllerを作成します。

php artisan make:controller ProfileController

作成されたapp/Http/Controllers/ProfileController.phpを次のように編集します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfileController extends Controller
{
    public function edit()
    {
        return view('profile-edit');
    }

    public function update(Request $request)
    {
        $name = $request->input('name');

        return '保存しました:' . e($name);
    }
}

edit()は、プロフィール編集フォームを表示するためのメソッドです。

update()は、フォームから送信された名前を受け取るためのメソッドです。

ここでは学習用に文字列を返しています。実際のアプリでは、この場所でデータベースへ保存する処理を書きます。

手順2:ルートを作る

次に、routes/web.phpにルートを書きます。

<?php

use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/profile/edit', [ProfileController::class, 'edit'])
    ->name('profile.edit');

Route::post('/profile/update', [ProfileController::class, 'update'])
    ->name('profile.update');

GET /profile/editは、フォームを表示するためのルートです。

POST /profile/updateは、フォームの送信内容を受け取るためのルートです。

プロフィール変更はデータを変更する処理です。そのため、GETではなくPOSTで受け取ります。

routes/web.phpに書いたルートは、通常webミドルウェアグループを通ります。そのため、セッション管理やCSRF保護の対象になります。

手順3:Bladeでフォームを作る

次に、フォーム画面を作ります。

resources/views/profile-edit.blade.phpを作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>プロフィール編集</title>
</head>
<body>
    <h1>プロフィール編集</h1>

    <form method="POST" action="{{ route('profile.update') }}">
        @csrf

        <label>
            名前:
            <input type="text" name="name">
        </label>

        <button type="submit">保存する</button>
    </form>
</body>
</html>

このフォームで重要なのは、次の部分です。

@csrf

@csrfを書くと、LaravelがCSRFトークン用のhidden項目を自動で出力します。

実際のHTMLでは、次のようなhidden項目になります。

<input type="hidden" name="_token" value="ランダムなトークン">

この_tokenが、フォーム送信時にサーバーへ送られます。

手順4:フォームを表示する

Laravelの開発サーバーを起動します。

php artisan serve

ブラウザで次のURLを開きます。

http://localhost:8000/profile/edit

プロフィール編集フォームが表示されます。

名前を入力して「保存する」を押すと、/profile/updateへPOSTリクエストが送られます。

フォームには@csrfがあるため、名前だけでなくCSRFトークンも一緒に送信されます。

手順5:サーバー側では何が確認されるのか

@csrfは、フォームにCSRFトークンを入れるための書き方です。

実際にリクエストを確認するのは、サーバー側のPreventRequestForgeryミドルウェアです。

Laravel 13では、まずSec-Fetch-Siteヘッダーによってリクエスト元を確認します。

リクエスト元の確認が通る場合は、その時点でリクエストが許可されます。

リクエスト元の確認が通らない場合は、CSRFトークンの確認に進みます。

そのとき、送信されたCSRFトークンがない、またはセッション内のトークンと一致しない場合は、リクエストが拒否されます。

つまり、Laravel 13では次の2つの確認が関係します。

  • Sec-Fetch-Siteヘッダーによるリクエスト元の確認
  • CSRFトークンによるフォーム送信の確認

この確認は、Controllerのupdate()が実行される前に行われます。

Laravelでサーバー側に書く処理

Laravelでは、基本的にCSRFトークンの比較処理をControllerに書く必要はありません。

サーバー側の確認は、LaravelのPreventRequestForgeryミドルウェアが行います。

そのため、Controllerでは通常どおり保存処理を書きます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfileController extends Controller
{
    public function update(Request $request)
    {
        $name = $request->input('name');

        // ここでプロフィール情報を保存する
        // 例:$request->user()->update(['name' => $name]);

        return '保存しました';
    }
}

このupdate()は、Laravelのリクエスト確認を通過したあとに実行される処理です。

CSRF保護を自分で毎回書くのではなく、Laravelのミドルウェアに任せます。

フォームに@csrfを書かなかった場合

フォームに@csrfを書かない場合、フォームからCSRFトークンが送信されません。

<form method="POST" action="{{ route('profile.update') }}">
    <input type="text" name="name">
    <button type="submit">保存する</button>
</form>

このフォームでは、_tokenが送信されません。

Laravel 13では、同一オリジンからのリクエストだと確認できる場合、CSRFトークン検証なしで許可されることがあります。

一方で、リクエスト元の確認が使えない場合や、確認が通らないリクエストでは、CSRFトークン検証が必要になります。

そのため、LaravelのBladeでPOSTPUTPATCHDELETEのフォームを書く場合は、@csrfを入れます。

Laravelの@csrfは何をしているのか

@csrfは、フォームにCSRFトークンを埋め込むためのBladeディレクティブです。

自分で次のようなhidden項目を書く代わりに、Laravelが自動で出力してくれます。

<input type="hidden" name="_token" value="{{ csrf_token() }}">

Bladeでは、次のように短く書けます。

@csrf

変更系のBladeフォームでは、@csrfを書きます。

CSRFトークンで確認できること

CSRFトークンは、Laravelがセッションごとに用意するランダムな値です。

本物のフォームには、そのトークンがhidden項目として入ります。

フォームを送信すると、入力内容と一緒にCSRFトークンも送られます。

サーバー側では、送られてきたトークンと、セッションに保存されているトークンを比較できます。

外部から同じ送信先へリクエストを送ることはできても、正しいCSRFトークンを知らなければ、CSRFトークン検証を通過できません。

Laravel 13では、このCSRFトークン検証に加えて、Sec-Fetch-Siteヘッダーによるリクエスト元の確認も使われます。

GETでデータを変更しない

CSRF対策では、GETでデータを変更しないことも重要です。

GETは、本来データを取得するためのリクエストです。ページを表示する、記事一覧を取得する、詳細ページを見る、といった処理に使います。

次のように、URLへアクセスするだけで名前を変更できる作りは避けます。

Route::get('/profile/update', function (Request $request) {
    $name = $request->query('name');

    return '保存しました:' . e($name);
});

このような作りにすると、URLを開くだけでデータが変更される可能性があります。

プロフィール変更、削除、購入、退会などの処理は、POSTPUTPATCHDELETEなどで扱います。

削除フォームにも@csrfを書く

記事削除のような処理でも、CSRF対策が必要です。

次のコードは、記事削除機能を作っていて、posts.destroyという名前付きルートと$post変数がある場合の例です。

<form method="POST" action="{{ route('posts.destroy', $post) }}">
    @csrf
    @method('DELETE')

    <button type="submit">削除する</button>
</form>

ここでも@csrfを書きます。

@method('DELETE')は、削除処理であることをLaravelに伝えるためのものです。

@csrfは、CSRFトークンをフォームに入れるためのものです。

役割が違うため、削除フォームでは両方を書きます。

JavaScriptで送信する場合のCSRF対策

フォームではなくJavaScriptでPOSTする場合も、CSRF対策を考える必要があります。

Laravelでは、X-CSRF-TOKENヘッダーにCSRFトークンを入れて送信できます。

まず、Bladeの<head>内にmetaタグを入れます。

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

JavaScript側では、その値を読み取ってヘッダーに入れます。

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

fetch('/profile/update', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': token,
    },
    body: JSON.stringify({
        name: '山田太郎',
    }),
})
    .then(response => response.text())
    .then(text => {
        console.log(text);
    });

この例では、/profile/updateへJSON形式で名前を送信しています。

Controller側では、通常のフォーム送信と同じように$request->input('name')で値を取得できます。

public function update(Request $request)
{
    $name = $request->input('name');

    return '保存しました:' . e($name);
}

通常のBladeフォームでは@csrfを使います。JavaScriptから送信する場合は、X-CSRF-TOKENヘッダーにCSRFトークンを入れる方法があります。

X-XSRF-TOKENヘッダー

Laravelでは、X-XSRF-TOKENヘッダーを使う方法も用意されています。

Laravelは、現在のCSRFトークンを暗号化したXSRF-TOKEN Cookieをレスポンスに含めます。

Axiosなど一部のJavaScriptライブラリは、このCookieの値を使ってX-XSRF-TOKENヘッダーを自動で送信できます。

通常のBladeフォームでは@csrfを使います。JavaScript送信では、X-CSRF-TOKENX-XSRF-TOKENヘッダーを使う方法があります。

CSRF保護を外す場合

Laravelでは、特定のURIをCSRF保護の対象外にできます。

ただし、通常のフォームではCSRF保護を外しません。

外部サービスから送られるWebhookなど、CSRFトークンを付けられないリクエストでは、例外設定が必要になることがあります。

たとえば、決済サービスのWebhookでは、LaravelのCSRFトークンではなく、そのサービスが送る署名を検証する形になることがあります。

Laravel 13では、bootstrap/app.phpで次のように例外URIを指定できます。

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})

通常のログイン後フォームでは、CSRF保護を外さず、@csrfを入れて送信します。

SameSite Cookieとの関係

CSRF対策では、CookieのSameSite属性も関係します。

SameSiteは、別サイトからのリクエストにCookieを送るかどうかを制御するCookieの属性です。

  • Strict:別サイトからのリクエストではCookieを送らない
  • Lax:一部の画面移動ではCookieを送るが、多くのCSRF対策として役立つ
  • None:別サイトからのリクエストにもCookieを送る。使う場合はSecureも必要

CSRFトークンは、サーバー側でリクエストを確認する対策です。

SameSite Cookieは、ブラウザ側でCookieの送信を制御する対策です。

Laravelのフォームでは、まず@csrfによるCSRFトークンの送信と、サーバー側のPreventRequestForgeryミドルウェアによる確認を理解することが大切です。

CSRF対策が必要なフォーム

LaravelでCSRF対策が必要になるのは、ログイン中のユーザーが実行する変更系のフォームです。

  • プロフィール変更フォーム
  • メールアドレス変更フォーム
  • パスワード変更フォーム
  • 記事投稿フォーム
  • 記事編集フォーム
  • 記事削除フォーム
  • 購入フォーム
  • 退会フォーム

表示するだけのページや、検索だけを行うフォームでは、CSRFトークンが必ず必要になるわけではありません。

データを変更するフォームでは、フォーム側に@csrfを書き、サーバー側ではLaravelのPreventRequestForgeryミドルウェアによってリクエストが確認される状態にしておきます。

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