サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

Laravelセキュリティ実務ガイド:認証と認可、2FA、CSP、ファイル、監査ログの設計

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

Laravelには認証、認可、CSRF対策、暗号化などの機能が用意されています。

ただし、機能を有効にしただけでは、ファイル公開、Webhook、アカウント復旧、テナント分離、ログ運用まで安全になるわけではありません。

本記事では、Laravelアプリを設計、実装、運用するときに確認したい対策を、判断理由とともに整理します。

コードと設定値は例です。

対象のLaravelバージョン、サービスのリスク、利用者、インフラ構成に合わせて調整し、テスト環境で確認してから本番へ反映してください。

最初に押さえる四つの考え方

対策の一覧へ進む前に、セキュリティ設計で使う言葉をそろえます。

実務では、資産を列挙し、そこへ到達する攻撃面と信頼境界を図にしたうえで、予防、検知、復旧の三段階に対策を置きます。

確認対象 起こり得る失敗 対策の例
ログイン 総当たり、認証情報の悪用 レート制限、多要素認証、異常検知
注文の更新 他人のデータを変更 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インジェクション

$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や業務キーで一意にします。

マルチテナントは複数の層で分離する

マルチテナントは、一つのシステムを複数の組織や顧客が共有しながら、データと権限を相互に分離する構成です。

分離方式には異なる費用と運用特性があり、単純な優劣では選べません。

方式 分離の単位 確認点
データベース分離 テナントごとにデータベースを分ける 接続管理、移行、バックアップ、コスト
スキーマ分離 同じ基盤でスキーマを分ける 対応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アクセシビリティを後付けにしないための工程別の確認方法は、発注、設計、実装、運用のチェックポイントも参考になります。

公開前チェックリスト

認証と認可

入力、ファイル、外部連携

ブラウザ、運用、データ

アクセシビリティ

参考資料

認証や権限、監査を個別対応で終わらせず、運用を含む一つの設計にまとめることが安全性を支えます。株式会社greedenはWebシステム開発で、要件定義から保守と改善まで継続的に支援しています。

モバイルバージョンを終了