N
NextTech Insights
Next.jsのCSP段階投入|Report-Onlyで壊さず導入する手順(チェックリスト)
securitywebops

Next.jsのCSP段階投入|Report-Onlyで壊さず導入する手順(チェックリスト)

5 min read

Next.jsでCSPを導入する実務手順。まずReport-Onlyで違反レポートを集め、依存を棚卸し、allowlistを締め、script-srcはnonce/hash方針を固めてから強制モードへ。ロールバック前提で進める。

目次

Next.jsでCSPを入れるなら、Report-Onlyでどう段階投入する?

結論(Conclusion)

安全な順序はこれ。

  1. Report-Onlyから開始(いきなり強制しない)
  2. 違反レポートを収集して依存を棚卸し
  3. allowlistを締めるhttps: → 特定ホストへ)
  4. script-srcは最後(nonce/hashへ寄せる方針を作る)
  5. ロールバック前提で強制モードへ切替

レポートを取らずに強制すると、auth/計測/埋め込みが壊れてCSPを諦めがち。

背景(Explanation)

CSPは「ヘッダーを足したら終わり」ではなく、ブラウザとの契約。 タグ、CDN、埋め込みが変わるたびに、ポリシーも追従が必要。

よくある失敗:

  • コピペCSPを強制
  • ログインや計測が壊れる
  • CSPごと撤去

Report-OnlyはCSPをOpsの段階投入に変える(観測→改善→強制)。

実務手順(Practical Guide)

手順1:外部依存を棚卸しする(CSPの材料)

  • analytics(GA4/GTM/Segment)
  • auth(OAuthリダイレクト)
  • images/CDN(S3/Cloudinary/画像プロキシ)
  • fonts(Google Fonts / self-host)
  • embeds(YouTube/Stripe/maps)

この一覧が人間が読めるallowlist。

手順2:最小のReport-Onlyを入れる

Content-Security-Policy-Report-Only:
  default-src 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'none';
  img-src 'self' data: https:;
  script-src 'self' 'unsafe-inline' https:;
  style-src 'self' 'unsafe-inline' https:;
  connect-src 'self' https:;

判断基準:

  • 最初は https: でもいい(観測フェーズ)。ただし締める前提。

手順3:Next.jsでヘッダーを入れる

// next.config.ts
import type { NextConfig } from 'next';

const cspReportOnly = [
  "default-src 'self'",
  "base-uri 'self'",
  "object-src 'none'",
  "frame-ancestors 'none'",
  "img-src 'self' data: https:",
  "script-src 'self' 'unsafe-inline' https:",
  "style-src 'self' 'unsafe-inline' https:",
  "connect-src 'self' https:",
].join('; ');

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [{ key: 'Content-Security-Policy-Report-Only', value: cspReportOnly }],
      },
    ];
  },
};

export default nextConfig;

手順4:違反レポートを収集する(最小実装)

// app/api/csp-report/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  let body: unknown = null;
  try {
    body = await req.json();
  } catch {
    body = await req.text();
  }

  console.log('[csp-report]', body);
  return NextResponse.json({ ok: true });
}

ポリシーに追加:

...; report-uri /api/csp-report;

手順5:レポートを見てallowlistを締める

分類する:

  • 必要(許可)
  • 不要(削除/置換)
  • 混入(怪しいタグ、古いスクリプト)

順番:

  1. img-src / connect-src から締める
  2. script-src は最後

手順6:script-srcをnonce/hashへ寄せる(難所)

'unsafe-inline' を抜くのが本丸。

  • インラインを減らして外部化
  • 残る分は nonce/hash

判断基準:

  • 「インラインがどこから来ているか」説明できないなら、強制はまだ早い。

手順7:強制モードへ切替(ロールバック前提)

切替条件:

  • 主要フローがReport-Onlyで問題なし(ログイン/計測/フォーム/決済)
  • https: から特定ホストへ寄せられている
  • 戻せるトグルがある

切替:

  • Content-Security-Policy-Report-OnlyContent-Security-Policy

失敗パターン(Pitfalls)

  • 強制を急いでauth/計測が壊れる
  • https: を許したまま放置
  • frame-ancestors 'none' で正当な埋め込みが死ぬ
  • レポート受信でPII/秘密をログに残す

チェックリスト(Checklist)

  • [ ] 外部依存(計測/認証/CDN/フォント/埋め込み)を棚卸しした
  • [ ] CSP Report-Only をデプロイした
  • [ ] レポート受信エンドポイントを用意した
  • [ ] レポートを週次でレビューして分類している
  • [ ] img-src/connect-src を特定ホストへpinした
  • [ ] インラインscriptを減らし外部化した
  • [ ] nonce/hash方針が決まっている
  • [ ] frame-ancestors の適用範囲を決めた
  • [ ] 主要フローをCSP下でテストした
  • [ ] ロールバック手段(トグル)がある
  • [ ] 条件を満たしてから強制モードへ切り替える

FAQ

Q1. いきなり強制CSPを入れていい?

ダメ。Report-Onlyで観測してから締める。強制から入ると壊れて撤去になりやすい。

Q2. 最初に https: を許すのはアリ?

観測フェーズならアリ。ただし最終形ではない。必ず特定ホストへ寄せる。

Q3. script-srcが一番難しいのはなぜ?

インラインscriptやタグの混入が多いから。外部化かnonce/hashの方針がないと強制できない。

参考

人気記事

  1. 1Permit2とは何か?Approveが変わった理由と安全な使い方(チェックリスト)
  2. 2署名(Sign)画面の読み方|Approve/Permit/NFT全権限を30秒で判別するチェックリスト
  3. 3仕様→実装に落とすプロンプトテンプレ(AI開発)|AIに“迷わせない”仕様の書き方
  4. 4EVMのトークン承認(Approve)を見直す方法|Revokeの手順と判断基準(チェックリスト)
  5. 5曖昧な依頼を要件定義に変換する質問リスト(AI開発)|AIに実装させる前に聞くべきこと

関連記事