securitywebops
Next.jsのCSP段階投入|Report-Onlyで壊さず導入する手順(チェックリスト)
|
5 min read
Next.jsでCSPを導入する実務手順。まずReport-Onlyで違反レポートを集め、依存を棚卸し、allowlistを締め、script-srcはnonce/hash方針を固めてから強制モードへ。ロールバック前提で進める。
目次
- 結論(Conclusion)
- 背景(Explanation)
- 実務手順(Practical Guide)
- 手順1:外部依存を棚卸しする(CSPの材料)
- 手順2:最小のReport-Onlyを入れる
- 手順3:Next.jsでヘッダーを入れる
- 手順4:違反レポートを収集する(最小実装)
- 手順5:レポートを見てallowlistを締める
- 手順6:script-srcをnonce/hashへ寄せる(難所)
- 手順7:強制モードへ切替(ロールバック前提)
- 失敗パターン(Pitfalls)
- チェックリスト(Checklist)
- FAQ
- Q1. いきなり強制CSPを入れていい?
- Q2. 最初に https: を許すのはアリ?
- Q3. script-srcが一番難しいのはなぜ?
- 内部リンク(Internal links)
- 参考
Next.jsでCSPを入れるなら、Report-Onlyでどう段階投入する?
結論(Conclusion)
安全な順序はこれ。
- Report-Onlyから開始(いきなり強制しない)
- 違反レポートを収集して依存を棚卸し
- allowlistを締める(
https:→ 特定ホストへ) - script-srcは最後(nonce/hashへ寄せる方針を作る)
- ロールバック前提で強制モードへ切替
レポートを取らずに強制すると、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を締める
分類する:
- 必要(許可)
- 不要(削除/置換)
- 混入(怪しいタグ、古いスクリプト)
順番:
img-src/connect-srcから締めるscript-srcは最後
手順6:script-srcをnonce/hashへ寄せる(難所)
'unsafe-inline' を抜くのが本丸。
- インラインを減らして外部化
- 残る分は nonce/hash
判断基準:
- 「インラインがどこから来ているか」説明できないなら、強制はまだ早い。
手順7:強制モードへ切替(ロールバック前提)
切替条件:
- 主要フローがReport-Onlyで問題なし(ログイン/計測/フォーム/決済)
https:から特定ホストへ寄せられている- 戻せるトグルがある
切替:
Content-Security-Policy-Report-Only→Content-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の方針がないと強制できない。
内部リンク(Internal links)
- 親記事(Hub):AI開発:まずはここから
- 関連記事:
参考
- MDN: Content Security Policy: https://developer.mozilla.org/docs/Web/HTTP/CSP
- MDN: CSP violation reports: https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/report-to