Laravelには認証、認可、CSRF対策、暗号化などの機能が用意されています。
ただし、機能を有効にしただけでは、ファイル公開、Webhook、アカウント復旧、テナント分離、ログ運用まで安全になるわけではありません。
本記事では、Laravelアプリを設計、実装、運用するときに確認したい対策を、判断理由とともに整理します。
コードと設定値は例です。
対象のLaravelバージョン、サービスのリスク、利用者、インフラ構成に合わせて調整し、テスト環境で確認してから本番へ反映してください。
最初に押さえる四つの考え方
対策の一覧へ進む前に、セキュリティ設計で使う言葉をそろえます。
- 資産:アカウント、個人情報、注文、決済結果、管理権限など、漏えい、改ざん、停止から守る対象です。
- 攻撃面:ログイン画面、API、ファイルアップロード、Webhookなど、外部から入力や操作を受ける場所です。
- 信頼境界:ブラウザとサーバー、アプリと外部API、テナント同士など、権限やデータの扱いが変わる境目です。
- 多層防御:一つの対策が抜けても、別の対策で被害を止める考え方です。
実務では、資産を列挙し、そこへ到達する攻撃面と信頼境界を図にしたうえで、予防、検知、復旧の三段階に対策を置きます。
| 確認対象 | 起こり得る失敗 | 対策の例 |
|---|---|---|
| ログイン | 総当たり、認証情報の悪用 | レート制限、多要素認証、異常検知 |
| 注文の更新 | 他人のデータを変更 | Policyによるサーバー側認可、監査ログ |
| ファイル | 偽装ファイル、非公開資料の露出 | 内容検証、隔離、非公開保存、権限確認 |
| Webhook | なりすまし、再送による二重処理 | 署名検証、再送判定、冪等化 |
認証、認可、再認証を分ける
認証は利用者が誰かを確かめる処理で、認可はその利用者が対象の操作を許されているかを確かめる処理です。
ログイン済みでも、別の利用者の注文を更新してよいとは限らないため、画面でボタンを隠すだけでなく、サーバー側のPolicyやGateで毎回認可します。
メールアドレス変更、支払先変更、認証器の追加など影響の大きい操作では、ログイン状態だけに頼らず、パスワードや多要素認証による再認証を挟みます。
パスワードの方針
パスワードは、文字種を機械的に増やすより、十分な長さ、既知の弱い値を拒否するブロックリスト、試行回数の制限を組み合わせて守ります。
NIST SP 800-63B-4は、単一要素として使うパスワードを15文字以上、多要素認証の一部として使う場合を8文字以上とし、パスワードマネージャー、自動入力、貼り付けを許可するよう定めています。
従来の「12文字以上」という一律の決め方ではなく、認証方式と採用する指針を明示して方針を決めます。
Laravelでは平文を保存せず、Hashファサードでハッシュ化します。
計算コストを変更した場合は、ログイン成功後など平文を検証できる時点で再ハッシュを判断できます。
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($plain);
$user->save();
}
この処理で扱う平文や認証情報をログへ出力してはいけません。
セッションの固定化とCookie
ログイン成功後はセッションIDを再生成し、ログアウト時はセッションを無効化してCSRFトークンも再生成します。
Laravelの認証ドキュメントにも、認証成功後にセッションを再生成する例があります。
CookieにはHTTPS環境でSecureを付け、HttpOnlyと適切なSameSite属性を設定します。
Redisなどの共有ストアは複数サーバー間でセッションを共有する手段ですが、採用しただけでセッションが安全になるわけではありません。
有効期限、失効、暗号化通信、ストアへのアクセス制御まで一組として設計します。
Sanctumの役割
Sanctumは、同一サイトのSPAではCookieベースのセッション認証を扱い、モバイルアプリや外部クライアントでは個別トークンを扱えます。
トークンのabilityは操作範囲を狭める手掛かりですが、利用者がその注文を更新できるかというリソース単位の認可を置き換えません。
特にSanctumの公式ドキュメントでは、ファーストパーティーSPAからの要求に対してtokenCanが常にtrueを返すことが説明されています。
所有関係や役割はPolicyでも確認します。
public function update(User $user, Order $order): bool
{
return $order->user_id === $user->id
&& $user->tokenCan('orders:update');
}
LaravelのPolicyへ認可規則を集約すると、画面、API、ジョブから同じ判断を再利用できます。
2FAとアカウント復旧を一つの流れにする
2FAは、パスワードとは異なる要素も使って本人確認を行う仕組みです。
Fortifyを使う構成ではTOTPと復旧コードを利用できます。
WebAuthnでは端末の認証機能やセキュリティキーを認証器として使えますが、導入時は対応環境、登録解除、紛失時の復旧まで設計します。
認証器を失った利用者が復旧できなければ、強い認証を回避する運用が生まれます。
- 復旧コードは有効化時に一度だけ安全に提示し、再表示の条件を明確にします。
- 使用済みの復旧コードは無効化し、保存時は平文のまま保持しません。
- 認証器の追加と削除は再認証の対象にし、変更を監査ログへ残します。
- サポートによる復旧では、担当者が独断で認証を解除できない確認手順を定めます。
- 秘密の質問を代替認証に使わず、利用者が選べる複数の復旧手段を検討します。
入力画面を操作しやすくする
確認コード欄には見えるlabelを付け、入力の目的、形式、エラーをプログラムから関連付けます。
エラー後は該当項目へ移動できるようにし、色だけで状態を示しません。
<label for="otp">認証アプリの確認コード</label>
<p id="otp-help">表示されたコード、または復旧コードを入力してください。</p>
<input
id="otp"
name="code"
inputmode="numeric"
autocomplete="one-time-code"
aria-describedby="otp-help otp-error"
aria-invalid="true"
>
@error('code')
<p id="otp-error" role="alert">{{ $message }}</p>
@enderror
role=”alert”は、送信を妨げるエラーの通知に使います。
処理中や完了の通知にはrole=”status”を使い、読み上げを必要以上に割り込ませないようにします。
モーダルを使う場合は、開いたときのフォーカス移動、閉じた後の戻り先、Escapeキーでの操作も確認します。
試行回数を制限する
ログインと確認コードの試行には、アカウントと接続元を考慮したレート制限を設けます。
応答文はアカウントの存在を推測しにくい内容にそろえ、再試行できる時刻や次の手段を示します。
RateLimiter::for('login', function ($request) {
$email = strtolower((string) $request->input('email'));
return Limit::perMinute(5)->by($email.'|'.$request->ip());
});
回数や時間は例であり、管理者アカウント、一般利用者、APIで同じ値にする必要はありません。
短時間の集中を抑えるレート制限と、期間全体の利用上限の違いは、Laravelのレート制限とクォータの設計で詳しく解説しています。
入力と出力は文脈ごとに扱う
バリデーションは、入力が業務上受け入れられる形式かを確認する処理です。
一方、エスケープはHTML、URL、JavaScriptなど出力先の文脈に合わせて文字を安全に扱う処理です。
どちらか一方だけでは、入力と出力の安全を説明できません。
XSS、CSRF、SQLインジェクション
- Bladeの通常の波括弧はエスケープされますが、エスケープしない出力は、管理された安全なHTMLに限ります。
- Markdownやリッチテキストは、許可する要素と属性を決めてサニタイズします。
- ブラウザのCookieで認証する更新要求にはCSRF対策を適用します。
- Eloquentやクエリビルダーを使っても、列名や並び順を利用者の入力から直接組み立てず、許可リストで選びます。
- ユーザー入力をBladeディレクティブやクライアント側テンプレートの式として実行しません。
$allowed = ['created_at', 'score'];
$column = in_array($request->string('sort')->toString(), $allowed, true)
? $request->string('sort')->toString()
: 'created_at';
$query->orderBy($column, 'desc');
通常の例だけでなく、空文字、極端に長い入力、重複、文字コード、構造が崩れたデータもテストします。
想定外の入力を継続して探す方法は、Laravelで行うPestのファズテストも参考になります。
ファイルは公開前に検査する
拡張子は利用者が変更できるため、ファイルの内容、検出されたMIMEタイプ、サイズをサーバー側で確認します。
$request->validate([
'file' => [
'required',
'file',
'mimetypes:image/jpeg,image/png,application/pdf',
'max:8192',
],
]);
この許可形式と上限は例であり、サービス要件に合わせて狭くします。
検証を通過したファイルも、元のファイル名を公開パスに使わず、非公開ストレージへ保存します。
画像の位置情報など不要なメタデータは除去し、ウイルススキャンが必要な用途では、結果が出るまで隔離領域から配布しません。
ダウンロード時は利用者と対象ファイルの関係を認可してから、短い期限のURLやアプリ経由の配信を使います。
署名付きURLは改ざんと期限を検証する仕組みであり、「誰が閲覧してよいか」という認可の代わりにはなりません。
Webhookと外部通信を境界として守る
Webhookは外部システムからアプリへ処理を持ち込む入口です。
受信時は、生のリクエスト本文に対する署名、送信時刻やnonce、イベントIDを確認し、署名が正しくても同じイベントを二重処理しないようにします。
$payload = $request->getContent();
$provided = (string) $request->header('X-Signature');
$expected = hash_hmac(
'sha256',
$payload,
config('services.partner.secret')
);
abort_unless(hash_equals($expected, $provided), 401);
この例は署名比較だけを示しています。
実運用では、許容時間、再送判定、鍵の保管とローテーション、署名方式のバージョンも契約として決めます。
受理したイベントは永続的な一意キーで記録し、同じイベントIDに対する更新や通知が一度だけ成立するようにします。
SSRFを防ぐ
SSRFは、アプリに任意のURLへ接続させ、外部から到達できない内部サービスなどへ要求を送らせる攻撃です。
利用者がURLを指定できる機能では、許可する宛先を絞り、内部IPやクラウドのメタデータサービスを拒否し、名前解決後の接続先とリダイレクト先も検査します。
接続と応答のタイムアウト、リダイレクト回数、応答サイズにも上限を置きます。
ブラウザ向けの防御を段階導入する
HTTPSとレスポンスヘッダーは、アプリの認証や入力検証を補う防御です。
| ヘッダー | 主な役割 | 導入時の注意 |
|---|---|---|
| Content-Security-Policy | スクリプトや画像などの読込元を制限する | 先にReport-Onlyで違反を観測し、必要な資源を洗い出す |
| Strict-Transport-Security | 以後の接続をHTTPSへ限定する | includeSubDomainsとpreloadは全サブドメインのHTTPS対応を確認してから使う |
| X-Content-Type-Options | MIMEタイプの推測を抑える | 正しいContent-Typeを返す |
| Referrer-Policy | 遷移先へ送る参照元情報を制御する | 分析や外部連携への影響を確認する |
| Permissions-Policy | カメラや位置情報などの機能を制限する | 実際に必要な機能だけ許可する |
CSPはページが読み込む資源に合わせて作り、最初から他のサイトの設定をコピーしません。
MDNのCSPガイドが説明するように、Content-Security-Policy-Report-Onlyではポリシーを強制せずに違反を収集できます。
$policy = "default-src 'self'; "
."base-uri 'self'; "
."object-src 'none'; "
."frame-ancestors 'none'; "
."script-src 'self' 'nonce-{$nonce}'";
return $response->header('Content-Security-Policy', $policy);
nonceは要求ごとに予測できない値を生成し、その要求で許可するscript要素だけに付けます。
HSTSのincludeSubDomainsとpreloadは復旧が難しい設定なので、すべてのサブドメインをHTTPSで運用できることを確認してから採用します。
条件はMDNのHSTS解説で確認できます。
キューと定期処理は再実行を前提にする
冪等性は、同じ要求を繰り返しても、業務上の結果が重複しない性質です。
キューはタイムアウトやワーカー停止によって再実行され得るため、メール送信、決済、在庫更新などの副作用をイベントIDや業務キーで一意にします。
- 一時的な失敗と恒久的な失敗を分け、再試行できる例外だけを戻します。
- 回数、待機時間、最終失敗後の扱いを決めます。
- 外部APIの呼び出しにはタイムアウトとレート制限を設定します。
- ジョブのペイロードへ秘密情報や必要以上の個人情報を入れず、識別子から実行時に取得します。
- 一意制約やロックを使う場合も、ロック失効後の再実行をテストします。
マルチテナントは複数の層で分離する
マルチテナントは、一つのシステムを複数の組織や顧客が共有しながら、データと権限を相互に分離する構成です。
分離方式には異なる費用と運用特性があり、単純な優劣では選べません。
| 方式 | 分離の単位 | 確認点 |
|---|---|---|
| データベース分離 | テナントごとにデータベースを分ける | 接続管理、移行、バックアップ、コスト |
| スキーマ分離 | 同じ基盤でスキーマを分ける | 対応DB、マイグレーション、権限 |
| 行分離 | 共有テーブルをtenant_idで分ける | すべての読込、更新、集計で条件を強制する仕組み |
行分離でグローバルスコープを使う場合も、管理処理やバッチでスコープを外す経路がないかを確認します。
Policy、データベース制約、自動テストを重ね、キャッシュキー、セッション、キュー、検索インデックス、ストレージパスにもテナント識別子を含めます。
テナントAの識別子を使ってテナントBのデータへ到達できないことを、一覧、詳細、更新、エクスポートの各経路で試験します。
ログは調査に必要な情報だけ残す
構造化ログは、項目名と値を一定の形式で記録し、検索や集計をしやすくしたログです。
request_id、イベント名、結果、対象の内部IDは調査を助けますが、IPアドレス、氏名、メールアドレス、金額などは目的と保存期間を決めてから記録します。
パスワード、セッショントークン、APIキー、復旧コード、Webhookの秘密鍵は記録しません。
Log::info('order.created', [
'request_id' => request()->header('X-Request-Id'),
'order_id' => $order->id,
'actor_id' => auth()->id(),
'result' => 'success',
]);
監査ログでは、誰が、いつ、何に、どの操作を行い、成功したかを追跡できるようにします。
認証器の追加と削除、権限変更、データ出力、復旧操作など、影響の大きい操作を対象にします。
アプリケーションの通常ログとは保存先や閲覧権限を分け、改ざんの検知、保存期間、削除手順も決めます。
秘密情報とバックアップを復旧まで設計する
.envファイルをリポジトリへ含めず、本番の秘密情報は利用する環境から必要なプロセスだけが取得できるようにします。
秘密情報管理サービスやKMSを使う場合も、読取権限、利用記録、ローテーション、障害時の手順が必要です。
暗号鍵を変更するときは、新しい鍵で暗号化し、移行中だけ古い鍵でも復号できる期間を設け、移行完了後に古い鍵を失効させます。
バックアップは暗号化し、復号に必要な鍵を同じ場所へ置きません。
バックアップの作成成功だけでなく、隔離した環境へ実際に復元し、アプリが起動して必要なデータを読めることまで定期的に確認します。
実装する順序
すべてを同時に変更すると、障害の原因と対策の効果を追いにくくなります。
| 段階 | 作業 | 完了の判断 |
|---|---|---|
| 1 | 資産、攻撃面、利用中のLaravelと依存パッケージを棚卸しする | 担当者と優先順位が決まっている |
| 2 | 認証、認可、復旧、管理者操作を試験する | 不正な利用者と別テナントから拒否される |
| 3 | ファイル、Webhook、外部通信、キューの境界を固める | 偽装、再送、タイムアウト、重複を再現できる |
| 4 | CSPなどのヘッダーを観測モードから導入する | 必要な資源を壊さず、想定外の読込を把握できる |
| 5 | 監査ログ、アラート、バックアップ復元を運用する | 担当者が手順に沿って検知と復旧を実行できる |
Webアクセシビリティを後付けにしないための工程別の確認方法は、発注、設計、実装、運用のチェックポイントも参考になります。
公開前チェックリスト
認証と認可
- パスワード方針は採用する指針と認証方式に合っている
- ログイン成功後にセッションIDを再生成する
- 重要操作で再認証を行う
- 画面とAPIの両方でPolicyによる認可を行う
- 2FAの登録、利用、紛失、解除を一連の流れとして試験する
入力、ファイル、外部連携
- 入力検証と出力時のエスケープを分けて実装する
- 動的な列名や並び順は許可リストから選ぶ
- ファイルを非公開領域へ保存し、検査後に配布する
- Webhookの署名、時刻、再送、イベントIDを検証する
- 外部接続の宛先、タイムアウト、リダイレクトを制限する
ブラウザ、運用、データ
- CSPをReport-Onlyで確認してから強制する
- HSTSの対象となるサブドメインを確認する
- キューの再試行で副作用が重複しない
- テナント境界を一覧、詳細、更新、出力で試験する
- ログに秘密情報や不要な個人情報を残さない
- バックアップから復元できることを確認する
アクセシビリティ
- 入力欄に見えるlabelと具体的な説明がある
- エラーを色だけで示さず、項目と関連付ける
- キーボードだけで登録、ログイン、2FA、復旧を完了できる
- モーダルを閉じた後にフォーカスが適切な場所へ戻る
- 画像CAPTCHAだけを唯一の手段にしない
- 動きを使う場合はprefers-reduced-motionを尊重する
参考資料
- Laravel Authentication
- Laravel Authorization
- Laravel Sanctum
- Laravel Hashing
- Laravel Rate Limiting
- Laravel Validation
- Laravel URL Generation
- MDN Content Security Policy
- MDN Strict-Transport-Security
- NIST SP 800-63B-4
- OWASP Application Security Verification Standard
- Web Authentication Level 3
- RFC 6238 TOTP
この記事に関連する株式会社greedenの取り組み
認証や権限、監査を個別対応で終わらせず、運用を含む一つの設計にまとめることが安全性を支えます。株式会社greedenはWebシステム開発で、要件定義から保守と改善まで継続的に支援しています。

