securitywebops
Next.jsのセキュリティヘッダー|何から入れる?実務チェックリスト(HSTS / CSP Report-Only / COOP/COEP)
|
5 min read
Next.jsでセキュリティヘッダーを入れる実務順序。低リスクの基本ヘッダー→HSTS(HTTPSが前提)→CSPはReport-Onlyで観測→COOP/COEP/CORPは必要なルートだけ。壊さず段階投入するためのチェックリスト。
目次
- 結論(Conclusion)
- 背景(Explanation)
- 実務手順(Practical Guide)
- 手順1:外部依存を棚卸しする(CSPの材料)
- 手順2:安全な基本ヘッダーを入れる(低リスク)
- 手順3:HSTSを入れる(HTTPSが絶対条件)
- 手順4:CSPはReport-Onlyから始める
- 手順5:COOP/COEP/CORPは必要なルートだけ
- 手順6:段階投入(Ops)
- 失敗パターン(Pitfalls)
- チェックリスト(Checklist)
- FAQ
- Q1. helmetを入れれば終わり?
- Q2. CSPはなぜReport-Onlyから?
- Q3. COOP/COEPはいつ必要?
- 内部リンク(Internal links)
- 参考
Next.jsでセキュリティヘッダーは何から入れる?順序は?
結論(Conclusion)
壊しにくい順に入れる。
- 安全な基本ヘッダー(nosniff / referrer / permissions)
- HSTS(HTTPSが全域で保証できる場合のみ)
- CSPはReport-Only(観測→締める→強制)
- COOP/COEP/CORP は“必要なルートだけ”(クロスオリジン分離が必要な場合)
実務ルール:1デプロイ=1変更くらいで段階投入して戻せるようにする。
背景(Explanation)
セキュリティヘッダーは「入れたら終わり」ではなく、ブラウザとの契約。 壊れやすいのはだいたいここ。
- authリダイレクト
- 計測タグ
- 埋め込み
- CDN/フォント/画像
だからヘッダー変更はOpsの段階投入として扱う(観測して、戻せるようにする)。
実務手順(Practical Guide)
手順1:外部依存を棚卸しする(CSPの材料)
- analytics(GA4/GTM)
- images/CDN(Cloudinary/S3/画像プロキシ)
- fonts(Google Fonts / self-host)
- embeds(YouTube/Stripe/maps)
- auth(リダイレクト先)
この一覧がallowlistの元。
手順2:安全な基本ヘッダーを入れる(低リスク)
X-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: ...X-Frame-Options: DENY(埋め込みが不要なら)
Next.js例:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'X-Frame-Options', value: 'DENY' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=()',
},
],
},
];
},
};
export default nextConfig;
判断基準:
- 埋め込みが必要なら
X-Frame-Optionsは避けて、CSPのframe-ancestorsで制御する。
手順3:HSTSを入れる(HTTPSが絶対条件)
Strict-Transport-Security: max-age=...; preload
判断基準:
- どこかにHTTPが残っているなら入れない(閉じ込め事故)。
手順4:CSPはReport-Onlyから始める
- まず
Content-Security-Policy-Report-Only - いきなり
Content-Security-Policy(強制)はやらない
最小の叩き台:
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:;
レポートを見て締める:
'unsafe-inline'を減らすhttps:を放置せず、既知ホストにpinする- scriptはnonce/hashへ
手順5:COOP/COEP/CORPは必要なルートだけ
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corpCross-Origin-Resource-Policy: same-site
判断基準:
- SharedArrayBuffer等でクロスオリジン分離が必要なルートだけに適用する。
手順6:段階投入(Ops)
- 1デプロイ=1変更
- ロールバック手段を用意
- 主要フロー(ログイン/計測/埋め込み)を毎回確認
失敗パターン(Pitfalls)
- コピペCSPでauth/計測が壊れる
- HTTPが残っているのにHSTSを入れて閉じ込める
- COEPを全体に入れて埋め込み/CDNが壊れる
- 埋め込みが必要なのに
X-Frame-Options: DENY
チェックリスト(Checklist)
- [ ] 外部依存(計測/認証/CDN/フォント/埋め込み)を棚卸しした
- [ ] 基本ヘッダーを入れた(nosniff/referrer/permissions)
- [ ] 埋め込み方針を決めた(XFO or frame-ancestors)
- [ ] HTTPSが全域で保証されていることを確認した(HSTS前提)
- [ ] HSTSのmax-age戦略を決めた(短→長、preload可否)
- [ ] CSPはReport-Onlyから開始した
- [ ] CSPレポートの受け皿がある
- [ ] allowlistを
https:から既知ホストへ締めた - [ ] script-srcの方針がある(外部化/nonce/hash)
- [ ] COOP/COEP/CORPは必要ルートだけに限定した
- [ ] ロールバック手段がある
FAQ
Q1. helmetを入れれば終わり?
ベースラインにはなるが、埋め込み方針やCSP allowlist、段階投入はアプリ固有。最後は意思決定が必要。
Q2. CSPはなぜReport-Onlyから?
アプリ固有だから。観測してから締めないと壊れる。
Q3. COOP/COEPはいつ必要?
クロスオリジン分離が必要な機能がある時だけ。分からないなら全体適用しない。
内部リンク(Internal links)
- 親記事(Hub):AI開発:まずはここから
- 関連記事:
参考
- MDN: Content Security Policy: https://developer.mozilla.org/docs/Web/HTTP/CSP
- MDN: Strict-Transport-Security (HSTS): https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security
- MDN: Permissions-Policy: https://developer.mozilla.org/docs/Web/HTTP/Headers/Permissions-Policy
- Web.dev: Cross-origin isolation: https://web.dev/cross-origin-isolation-guide/