こんにちは、かつコーチです。
前回はPolicy・Gateを使った認可の仕組みを扱いました。
今回は、Webアプリのセキュリティで必ず押さえておきたいCSRF(クロスサイトリクエストフォージェリ)対策について解説します。
Laravelは標準でCSRF対策の仕組みを持っていますが、「なぜそれが必要なのか」を理解していないと、フォーム送信でエラーが出たときに正しく対処できません。
仕組みから丁寧に見ていきましょう。
CSRFとは?どんな攻撃なのか
CSRF攻撃の仕組み
CSRF(Cross-Site Request Forgery:クロスサイトリクエストフォージェリ)とは、ログイン中のユーザーに気づかれないまま、悪意のあるリクエストを送信させる攻撃です。
具体的な流れはこうです。
- ユーザーが正規のサイト(例:オンラインバンキング)にログインし、セッションCookieが発行される
- ユーザーが別タブで悪意のあるサイトを開く
- その悪意のあるサイトに、正規サイトの「送金処理」を実行するフォームが仕込まれている
- ユーザーが気づかずページを開いただけで、ブラウザは正規サイトのCookieを自動的に付けてリクエストを送ってしまう
- 正規サイト側は「ログイン中のユーザー本人からのリクエスト」と誤認し、送金処理が実行されてしまう
ポイントは、ブラウザはドメインが違っても同じCookieを自動的に送ってしまうという性質を悪用している点です。
ユーザー本人は何も操作していないのに、意図しない処理が実行されてしまうのがCSRFの怖いところです。
なぜこの攻撃が成立してしまうのか
サーバー側が「ログイン中のセッションCookieが送られてきた」ことだけを根拠にリクエストを信頼してしまうと、この攻撃は簡単に成立します。
つまり、リクエストの送信元が「本当に自分のサイトのフォームから送られたものか」を検証する仕組みがないと、CSRFを防げません。
この検証のために使われるのが、CSRFトークンという仕組みです。
CSRFトークンによる対策の仕組み
トークンで正規のリクエストだと証明する
CSRFトークンの考え方はシンプルです。
- サーバーがページを表示するときに、ランダムな文字列(トークン)を発行し、セッションに保存する
- 同じトークンをフォームの隠しフィールドにも埋め込む
- フォーム送信時、サーバーはリクエストに含まれるトークンと、セッションに保存したトークンが一致するかを確認する
- 一致すれば「正規のフォームから送信されたリクエスト」として処理を続行する
悪意のあるサイトは、このトークンの値を知る手段がありません(同一生成元ポリシーにより、他サイトから正規サイトのページ内容を読み取ることはできないため)。
そのため、トークンが一致しないリクエストは弾かれ、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のエスケープ処理を理解する