セキュリティ

Invalid CSRF tokenの意味と直し方|Laravel 419・Django・Spring Securityのエラーを切り分ける

フォームを送信した瞬間に「Invalid CSRF token」「intercepted」と英語で返され、入力内容ごと画面が消える。このメッセージは、サーバーが「このリクエストが本当に自サイトの画面から送られたのか確認できない」と判断し、処理に入る前に遮断したことを意味します。攻撃を受けたサインに見えますが、実際に多いのはセッション切れ・Cookieのブロック・複数タブといった正規利用者側の事故です。この記事では、メッセージそのものの読み方から、LaravelやDjango、Spring Securityが返す具体的なエラーの正体、利用者と開発者それぞれの切り分け手順までを整理します。

まとめ:Invalid CSRF tokenエラーの意味と最短の直し方

Invalid CSRF tokenは「照合用の使い捨て文字列(CSRFトークン)が合わない、または付いていない」というサーバー側の判定結果です。続くinterceptedは、そのリクエストがアプリケーションの処理に届く前に遮断されたことを指します。データは書き換わっていません。止まっているだけです。

利用者として遭遇したなら、フォームのページ自体を再読み込みしてから入力し直すのが最短の復旧手順になります。古いタブに残った画面をそのまま送信しても、トークンは既に新しくなっているため何度でも弾かれます。それでも直らなければ、Cookieをブロックする拡張機能とプライベートモードを疑ってください。

開発者として原因を追うなら、見る順番は決まっています。セッションの有効期限、複数サーバー構成でのセッション共有、リバースプロキシ配下のオリジン設定、そしてフォームを含むページのキャッシュ。この4つで大半が説明できます。以降、メッセージの構造、フレームワーク別の正体、原因、直し方の順に掘り下げます。

「Invalid CSRF token」「intercepted」というメッセージが指しているもの

エラー文が英語のまま出るため、何を咎められているのか掴みにくいのがこのメッセージの厄介なところです。単語ごとに分解すると、サーバーが何を確認して何に失敗したのかがはっきりします。

invalid・CSRF token・interceptedの語義分解

CSRF tokenは、サーバーが画面を返すときに埋め込む使い捨ての文字列です。ブラウザはフォーム送信時にこれを一緒に返し、サーバーは自分が発行した値と一致するかを照合します。外部サイトに置かれた罠のフォームはこの値を知り得ないため、照合を通せません。

invalidは、その照合に失敗した状態を指します。値が違う場合だけでなく、そもそも送られてこなかった場合、期限切れで破棄済みだった場合も同じ「invalid」に丸められる実装が大半です。だから「トークンは確かに送ったのに invalid と言われる」という状況が普通に起こります。

interceptedは「途中で捕まえた」という意味です。CSRF検証はコントローラーやビューの手前に置かれたミドルウェア/フィルタが担当するため、多くの実装では業務ロジックが動く前にリクエストが打ち切られます。裏返せば、このメッセージが出ている限り、送金や登録が中途半端に実行されている心配はありません。

なお、この英文をそのまま出力する主要フレームワークは存在しません。Invalid CSRF token intercepted.という一文は、業務SaaSなどが自前の実装で組み立てた製品固有の文言です。フレームワーク名で検索しても解決策が出てこないのはそのためで、対処は下記の一般的な原因から当たることになります。

攻撃の兆候か誤検知かを見分ける条件

自分の操作の直後に自分の画面で出たものは、まず誤検知と考えて構いません。本物のCSRF攻撃は、利用者が罠ページを開いた瞬間に背後で送信されるのが通常で、被害者がエラー画面を見る場面はほとんどありません。なお、罠ページ上で本人にボタンを押させるクリックジャッキングはトークン照合を通過してしまうため、このエラーでは検知できません。「エラーが見えている」こと自体が、正規の画面から正規の利用者が送信した傍証になりやすいわけです。

本当に警戒すべきなのは、サーバー側のログで特定のIPから短時間に大量の検証失敗が記録されているケースでしょう。この場合はアクセス元と対象エンドポイントを確認します。攻撃の成立条件そのものはCSRF(クロスサイトリクエストフォージェリ)とは何か?概要とそのリスクについて解説で仕組みから整理しました。混同されやすいXSSとの守備範囲の違いはXSSとCSRFの違いとは?仕組み・被害・対策を実装レベルで徹底比較にまとめてあります。

フレームワーク別に見るCSRFエラーの正体とHTTPステータス

表示される文言はフレームワークによってまるで違います。同じ現象なのに検索しても情報が噛み合わないのは、この差が原因です。手元のエラーがどれに当たるかを先に特定すると、調査範囲が一気に狭まります。

フレームワーク 表示・例外 ステータス 既定のトークン名
Laravel Page Expired / TokenMismatchException 419 _token / X-CSRF-TOKEN / XSRF-TOKEN
Django CSRF verification failed. Request aborted. 403 csrfmiddlewaretoken / X-CSRFToken
Spring Security InvalidCsrfTokenException / MissingCsrfTokenException 403 _csrf / X-CSRF-TOKEN

Laravel:419 Page Expiredとバージョンで入れ替わるミドルウェア

Laravelでトークン照合に失敗するとIlluminate\Session\TokenMismatchExceptionが投げられます。メッセージはCSRF token mismatch.です。フレームワークの例外ハンドラがこれをHTTP 419へ変換しており、Handler.phpには$e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e)という一行があります。419という見慣れないステータスの出どころはここです。画面に出るのは「Page Expired」になります。

照合を担うミドルウェアは、メジャーバージョンごとに名前も除外方法も変わりました。手元のプロジェクトでVerifyCsrfTokenが見つからないなら、原因は改名です。

バージョン ミドルウェア 除外の書き方
10 App\Http\Middleware\VerifyCsrfToken $except プロパティ
11・12 Illuminate\Foundation\Http\Middleware\ValidateCsrfToken validateCsrfTokens(except:)
13 Illuminate\Foundation\Http\Middleware\PreventRequestForgery preventRequestForgery(except:)

Laravel 13の新しいミドルウェアは、まずブラウザが送るSec-Fetch-Siteヘッダーを見ます。同一オリジンからのリクエストだと分かればトークン検証を行わずに通し、判定できないときだけ従来のトークン照合へ落とす二段構えです。公式ドキュメントは「The Sec-Fetch-Site header is only sent by browsers over secure (HTTPS) connections」と明記しており、HTTPで動く開発環境ではこの短絡路が働きません。ローカルだけ419が出るという食い違いは、ここから生まれます。サブドメイン間でフォームを送る構成ならallowSameSite: trueで同一サイトからの送信も許可できます。

オリジン検証だけに寄せてトークンを廃止することも可能です。この場合、失敗時のステータスは419ではなく403になります。

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(originOnly: true);
})

Ajax経路で詰まる場合は、トークンの運び方が2系統あることを押さえてください。metaタグの値をX-CSRF-TOKENヘッダーへ載せる方法と、Laravelが毎レスポンスで発行するXSRF-TOKENクッキーの値をX-XSRF-TOKENヘッダーへ載せる方法です。AxiosやAngularは後者を自動で行います。ただしこのクッキーは暗号化されており、暗号化キー(APP_KEY)を変更したり環境ごとに違う値を配ったりすると、発行済みのクッキーは一斉に復号できなくなります。デプロイ直後だけ419が多発するときは、まずキーの取り違えを疑ってください。

Django:CSRF verification failed.とREASON定数の読み方

DjangoのCsrfViewMiddlewareは検証に失敗するとHTTP 403を返し、画面には「Forbidden」という見出しと「CSRF verification failed. Request aborted.」という本文を出します。日本語ロケールの環境で「CSRF検証に失敗したため、リクエストは中断されました。」と表示されるのが、このmainにあたる文言です。

Djangoは失敗理由を定数として細かく分けており、原因の絞り込みが速く進みます。django/middleware/csrf.pyには次の文字列が並びます。

  • CSRF cookie not set.CSRF token missing. ── Cookieまたはトークン自体が届いていない
  • Origin checking failed - %s does not match any trusted origins. ── OriginヘッダーがCSRF_TRUSTED_ORIGINSと一致しない
  • Referer checking failed - no Referer.Referer checking failed - Referer is insecure while host is secure. ── HTTPS環境でRefererが欠落、またはhttpから来ている
  • has incorrect lengthhas invalid characters ── トークンの形が壊れている(途中で加工された疑い)

ところがこの理由はDEBUG = Trueのときしか画面に出ません。本番でエラーだけを見ても原因は分からないので、ログ側で拾います。CSRF拒否はdjango.security.csrfロガーに警告として記録されるため、出力先を繋いでおけば「Cookieが無いのか、Originが違うのか」を一発で切り分けられます。

# settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {
        "django.security.csrf": {"handlers": ["console"], "level": "WARNING"},
    },
}

Originの不一致で詰まるなら、確認するのはCSRF_TRUSTED_ORIGINSの書き方です。Django 4.0以降、この設定値にはスキームを含める必要があります。example.comではなくhttps://example.comと書きます。HTTPS終端をリバースプロキシやロードバランサに置いた構成で、アップグレード後に突然403が出るようになったなら、まずここを見てください。HTTPS運用ではCSRF_COOKIE_SECURE = Trueも併せて設定し、クッキーを置きたくない場合はCSRF_USE_SESSIONS = Trueでセッション側に保管する選択肢もあります。

Django REST FrameworkのSessionAuthenticationを使っている場合、返ってくる文言が変わります。DRFはrest_framework/authentication.pyCSRF Failed: (理由)という形式の403を返すため、Djangoの403ページは表示されません。APIだけがエラーになるならDRF側の経路を疑ってください。

Spring Security:InvalidCsrfTokenExceptionとBREACH対策で変わるトークン値

Spring Securityは照合に失敗するとInvalidCsrfTokenExceptionを投げ、403を返します。この例外のメッセージは受け取った値とパラメータ名・ヘッダー名を埋め込む形で組み立てられ、「Invalid CSRF Token ‘(値)’ was found on the request parameter ‘_csrf’ or header ‘X-CSRF-TOKEN’.」という文面になります。トークンがそもそもサーバー側に保存されていない場合はMissingCsrfTokenExceptionと別扱いです。例外クラスがどちらかで、原因の方向はまるで変わります。

Spring Security 6で追加設定が要るのはSPA構成です。既定のハンドラはXorCsrfTokenRequestAttributeHandlerで、BREACH攻撃対策としてトークン値にランダム性を混ぜ、リクエストごとに見た目が変わるよう符号化しています。一方、CookieCsrfTokenRepositoryXSRF-TOKENクッキーへ書き込むのは符号化前の生の値です。公式リファレンスも「JavaScript applications will only have access to the plain token value and will not have access to the encoded value」と書いています。JavaScriptがクッキーの値をそのままX-XSRF-TOKENヘッダーへ載せると、サーバーは符号化された値を期待しているため照合に失敗します。

さらに、認証成功時とログアウト成功時にはCsrfAuthenticationStrategyCsrfLogoutHandlerが古いトークンを破棄します。Spring Securityは既定でトークンの読み込みを遅延させるため、そのままでは新しいクッキーが返りません。公式リファレンスの表現では、クライアントは新しいトークンを取得するまでPOSTのような安全でないリクエストを実行できない状態になります。ログイン直後だけ通らない、という再現しにくい形で表面化します。

解決策はバージョンで分かれます。Spring Security 7.0以降csrf.spa()の一行で、クッキーの扱いと認証後のトークン再発行がまとめて解決します。6.x系にこのメソッドはありませんCsrfConfigurer#spa()は7.0で追加)。6.xでは公式ドキュメントの手順どおり、ヘッダー経由なら生の値、パラメータ経由なら符号化値を使い分けるSpaCsrfTokenRequestHandlerを自作して差し込みます。

// Spring Security 6.x(7.0 なら csrf.spa() の一行で済む)
http.csrf((csrf) -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
);

Spring Security 6系・7系の認証まわりの構成全体は、Spring Bootの認証をSpring Securityで実装する方法【Spring Security 6/7対応・2026年版】で扱っています。

3フレームワークのどれでもない「CSRF token verification failed.」の出どころ

検索でよく見かけるCSRF token verification failed.という文言は、上の3つのいずれも出力しません。DjangoのCSRF verification failed.と似ていますが、tokenの1語が入るかどうかで別物です。この文字列を出すのは、たとえばZammadのように自前で例外メッセージを定義しているアプリケーションです。

文言が一致しないなら、フレームワーク名ではなく製品名と組み合わせて検索したほうが確実に辿り着けます。原因と対処そのものは、以降で扱う一般的なパターンと共通です。

攻撃ではないのにエラーが出る典型原因

検証失敗の大半は、防御が正しく動いた結果ではなく、正規のリクエストが条件を満たせなかった結果です。発生源は利用者の環境側とサーバー構成側に分かれ、切り分けの入口も違います。

セッション切れ・Cookieブロック──利用者環境側の原因

最も多いのはセッションの期限切れです。フォームを開いたまま席を外し、戻ってきて送信した。それだけで、サーバー側のトークンは既に破棄されています。Laravelの既定セッション寿命はconfig/session.phplifetimeで120分、DjangoのSESSION_COOKIE_AGEは初期値1209600秒(2週間)です。この差が「あのサイトでは平気なのに」という体感の違いを生みます。

次に多いのがログイン絡みのトークン更新でしょう。Djangoの403ページにはDEBUG = Trueのときだけ表示されるHelp欄があり、そこに「After logging in in another browser tab or hitting the back button after a login, you may need to reload the page with the form, because the token is rotated after a login.」という一文が置かれています。別タブでログインし直した、ログイン後に戻るボタンで前の画面へ帰った。どちらもトークンが回転済みで、古いフォームは無効です。

Cookieが保存できない環境も定番の原因です。広告ブロックやトラッキング防止の拡張機能、サードパーティCookieを一律で拒否する設定、プライベートウィンドウ。いずれもトークンの保管先を潰すため、送信しても照合相手が存在しません。保存領域ごとの挙動差はCookie・localStorage・IndexedDBの違いと使い分けで比較しています。

セッション共有・キャッシュ──サーバー構成側の原因

複数台構成でセッションを共有していないケースが筆頭です。トークンを発行したサーバーと送信先になったサーバーが別だと、照合相手がどこにも無い状態になります。負荷が低い時間帯は再現せず、アクセスが増えて振り分けが分散した途端に多発する、という出方をします。RedisやDBへセッションを外部化すれば解消します。

フォームを含むページがCDNやリバースプロキシにキャッシュされている構成も危険です。ある利用者向けに発行されたトークンが埋まったHTMLを、別の利用者が受け取ります。ログイン後の画面とフォームを含むページは、キャッシュ対象から確実に外してください。

クッキー属性の設定ミスも見落とされがちです。認証CookieにSameSite=Strictを付けると、外部サイトからのリンクで遷移した直後の画面ではCookieが送られず、そこからの送信が通りません。決済サービスや外部IDプロバイダからのリダイレクトを挟む導線では、Laxが現実的な落としどころです。Secure属性を付けたCookieはHTTPS以外で送信されないため、HTTPのままの検証環境だけ再現するというねじれも起こります。なお、OriginヘッダーやCORSの設定と混同されやすい領域でもあります。両者の役割分担はCORSとは?仕組み・プリフライト・サーバー設定例・エラー解決を実装目線で解説で整理しました。

エラーを直すための切り分け手順

原因の候補が並んだところで、実際に潰していく順番を決めます。利用者として復旧したいのか、開発者として再発を止めたいのかで、やることは別物です。

利用者がその場でできる復旧手順

入力内容をコピーして退避してから、次の順に試します。上から順に、原因として多いものを潰していく並びです。

  • フォームのページを再読み込みし、新しい画面で入力し直して送信する
  • 同じサイトを開いている他のタブを閉じ、1つのタブだけで操作する
  • そのサイトのCookieを削除し、ログインからやり直す
  • 広告ブロックやプライバシー保護系の拡張機能を、そのサイトだけ無効にする
  • プライベートウィンドウや別ブラウザで再現するか確かめる

ここまでで直らず、しかも他の利用者も同じ状況なら、サイト側の不具合です。粘らずに問い合わせたほうが早く解決します。

開発者が原因を特定する順序

まず失敗理由を可視化します。Djangoなら前述のログ設定、Laravelなら419を返したリクエストのセッションID、Spring Securityなら投げられた例外クラスがどちらだったか。ここを飛ばして設定を触り始めると、当たりを引くまで時間を溶かします。

次にブラウザの開発者ツールで、送信されたリクエストの中身を実物で確認しましょう。見るのは3点です。トークンのフィールドまたはヘッダーが付いているか、Cookieが同時に送られているか、Originヘッダーの値が期待するホストと一致しているか。フィールドはあるのにCookieが無ければ保存側、両方あるのに落ちるならサーバー側の保管・共有を疑います。

再現条件が掴めたら、セッションストアの構成と対象ページのキャッシュ設定を確認します。仕上げに、トークンを外したリクエストがきちんと拒否されることも確かめておきたいところです。OWASP ZAPの診断項目とは?検出できる脆弱性一覧と使い方で扱うような診断ツールを使うと、更新系エンドポイント全体に検証が掛かっているかをまとめて点検できます。

CSRF検証を外してよい場面と、外してはいけない場面

エラーが止まらないとき、検証そのものを無効化する対処が検索上位にしばしば現れます。ここは立場をはっきりさせておきます。除外してよいのは、Cookieによる認証を使っていないエンドポイントだけです。

正当に除外できる代表例が、決済サービスなどからのWebhookです。外部システムはこちらのトークンを知りようがなく、そもそもブラウザのCookieを伴わないため、CSRFの成立条件から外れています。Laravelの公式ドキュメントもStripeのWebhookを例に挙げ、除外設定を案内しています。

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

Authorizationヘッダーでトークンを渡すAPIも同様です。ブラウザが自動で付与する資格情報に依存していない以上、CSRFトークンを重ねる意味はありません。この方式との違いはJWTとは?構造・署名検証の仕組みとセッション・OAuth/OIDCとの違いを実装視点で解説で扱っています。

逆に、管理画面・決済・退会・権限変更で検証を外すのは、規模を問わず選択肢に入りません。「一時的に無効化して様子を見る」も避けます。エラーの正体はほぼ設定ミスであり、無効化はその設定ミスを検出できなくするだけです。切り分けが長引くなら、無効化ではなくログを増やす方向へ進めてください。なお、テスト実行時についてはLaravelが全ルートでCSRFミドルウェアを自動的に無効化します。テストを通すために本番設定へ手を入れる必要はありません。

こうした設定は一度入れて終わりではなく、フレームワークのメジャー更新のたびに見直す対象です。Laravelのミドルウェア改名やSpring Securityのspa()追加のように、版が上がると前提が動きます。追随する担当者や仕組みが社内にないなら、システムの保守運用・内製化支援のように実装と運用を一体で任せる選択肢もあります。

よくある質問

「invalid csrf token intercepted.」とはどういう意味ですか?

「照合用のCSRFトークンが一致しなかったため、このリクエストを処理の手前で遮断した」という意味です。データの更新は実行されていません。この英文をそのまま出す主要フレームワークは無く、業務SaaSなどの独自実装による文言です。多くはセッション切れやCookieのブロックによる誤検知なので、ページを再読み込みして入力し直せば通ります。

このエラーが出たら攻撃を受けているのでしょうか?

自分の操作の直後に自分の画面へ出たものは、まず誤検知です。本物のCSRF攻撃は利用者に気づかせないまま背後で送信されるため、被害者がエラー画面を見る場面はほとんどありません。調べるべきは、同一の送信元IPから短時間に検証失敗が連続していないかという1点だけです。

Laravelの419エラーとCSRFエラーは同じものですか?

実質的に同じです。Laravelはトークン照合に失敗するとCSRF token mismatch.というメッセージのTokenMismatchExceptionを投げ、例外ハンドラがHTTP 419へ変換して「Page Expired」と表示します。ただしLaravel 13でオリジン検証のみのモードを有効にした場合、失敗時のステータスは419ではなく403になります。

「CSRF検証に失敗したため、リクエストは中断されました。」と表示されました

Djangoの403エラーページで、英語表記の「CSRF verification failed. Request aborted.」にあたります。原因の詳細はDEBUG = Trueのときしか画面に出ないため、本番環境ではdjango.security.csrfロガーの警告を確認してください。Cookie未設定・トークン欠落・Origin不一致のどれなのかが理由として記録されています。Django REST Framework経由ならCSRF Failed:という別形式になります。

SPAでXSRF-TOKENクッキーを送っているのにInvalid CSRF Tokenになります

Spring Security 6で起きる典型例です。既定のXorCsrfTokenRequestAttributeHandlerはトークンを符号化しますが、クッキーに書かれるのは符号化前の生の値のため、そのままヘッダーへ載せると照合に失敗します。7.0ならcsrf.spa()で解決しますが、6.xにこのメソッドは無く、SpaCsrfTokenRequestHandlerを自作してcsrfTokenRequestHandlerへ渡す必要があります。

関連記事

資料請求

RELATED POSTS 関連記事