Next.js アプリケーションにコンテンツセキュリティポリシー(CSP)を設定する方法
コンテンツセキュリティポリシー(CSP)は、クロスサイトスクリプティング(XSS)、クリックジャッキング、その他のコード注入攻撃など、さまざまなセキュリティ脅威から Next.js アプリケーションを保護するために重要です。
CSP を使用することで、開発者はコンテンツソース、スクリプト、スタイルシート、画像、フォント、オブジェクト、メディア(オーディオ、ビデオ)、iframe などの許可されるオリジンを指定できます。
Nonce
Nonce は、1 回限りの使用のために作成されたランダムな文字列です。これは CSP と組み合わせて使用し、特定のインラインスクリプトまたはスタイルを選別的に実行することで、厳密な CSP ディレクティブをバイパスできます。
Nonce を使用する理由
CSP は、攻撃を防ぐためにインラインと外部のスクリプトの両方をブロックできます。Nonce を使用することで、特定のスクリプトを安全に実行できます。ただし、一致する nonce 値を含めるスクリプトのみが対象になります。
攻撃者がページにスクリプトを読み込もうとした場合、nonce 値を推測する必要があります。このため、nonce は予測不可能で、すべてのリクエストに対して一意である必要があります。
Proxy を使用して Nonce を追加する
Proxy を使用することで、ページがレンダリングされる前にヘッダーを追加し、nonce を生成できます。
ページが表示されるたびに、新しい nonce を生成する必要があります。つまり、nonce を追加するには 動的レンダリングを使用する必要があります。
例を示します:
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
// 改行文字とスペースを置き換える
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
return response
}デフォルトでは、Proxy はすべてのリクエストで実行されます。matcher を使用して、Proxy を特定のパスでのみ実行するようにフィルタリングできます。
プリフェッチ(next/link から)と CSP ヘッダーが不要な静的アセットのマッチングを無視することをお勧めします。
export const config = {
matcher: [
/*
* 以下で始まるパスを除くすべてのリクエストパスをマッチする:
* - api(API ルート)
* - _next/static(静的ファイル)
* - _next/image(画像最適化ファイル)
* - favicon.ico(ファビコンファイル)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}Next.js での Nonce の動作方法
Nonce を使用するには、ページが動的にレンダリングされる必要があります。これは、Next.js がサーバーサイドレンダリング中にリクエストに含まれる CSP ヘッダーに基づいて nonce を適用するためです。静的ページはビルド時に生成されるため、リクエストまたはレスポンスヘッダーが存在しません。このため、nonce は挿入されません。
動的にレンダリングされるページでの nonce サポートの動作方法は以下の通りです:
- Proxy が nonce を生成する:proxy がリクエスト用に一意の nonce を作成し、
Content-Security-Policyヘッダーに追加し、カスタムx-nonceヘッダーにも設定します。 - Next.js が nonce を抽出する:レンダリング中に、Next.js は
Content-Security-Policyヘッダーを解析し、'nonce-{value}'パターンを使用して nonce を抽出します。 - Nonce が自動的に適用される:Next.js は nonce を以下に関連付けます:
- Framework スクリプト(React、Next.js ランタイム)
- ページ固有の JavaScript バンドル
- Next.js で生成されたインラインスタイルとスクリプト
nonceprop を使用する任意の<Script>コンポーネント
この自動動作により、各タグに手動で nonce を追加する必要はありません。
動的レンダリングを強制する
Nonce を使用している場合、ページを明示的に動的レンダリングに選択する必要があります:
import { connection } from 'next/server'
export default async function Page() {
// 受信リクエストを待機してこのページをレンダリングする
await connection()
// ページコンテンツ
}Nonce を読み込む
Server Component から headers を使用して nonce を読み込むことができます:
import { headers } from 'next/headers'
import Script from 'next/script'
export default async function Page() {
const nonce = (await headers()).get('x-nonce')
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}静的レンダリング と動的レンダリング(CSP 使用時)
CSP で nonce を使用することは、Next.js アプリケーションのレンダリング方法に重要な影響を及ぼします。
動的レンダリングの要件
CSP で nonce を使用する場合、すべてのページを動的にレンダリングする必要があります。つまり以下のことになります:
- ページは正常にビルドされますが、動的レンダリング用に適切に設定されていない場合、実行時エラーが発生する可能性があります
- 各リクエストで新しい nonce を含む新しいページが生成されます
- 静的最適化と増分静的再生成(ISR)は無効になります
- ページは追加の設定なしで、CDN によってキャッシュされません
- 部分的プリレンダリング(PPR)は nonce ベースの CSP と互換性がありません。静的シェルスクリプトが nonce にアクセスできないためです。
パフォーマンスへの影響
静的から動的レンダリングへの移行はパフォーマンスに影響します:
- 初回ページ読み込みが遅い:ページはリクエストごとに生成する必要があります
- サーバー負荷の増加:すべてのリクエストがサーバーサイドレンダリングを必要とします
- CDN キャッシングなし:動的ページはデフォルトではエッジでキャッシュされません
- ホスティングコストが高くなる:動的レンダリングにはより多くのサーバーリソースが必要です
Nonce を使用する場合
以下の場合に nonce の使用を検討してください:
'unsafe-inline'を禁止する厳密なセキュリティ要件がある- アプリケーションが機密データを処理している
- 他のインラインスクリプトをブロックしながら、特定のインラインスクリプトを許可する必要がある
- コンプライアンス要件で厳密な CSP が義務付けられている
Nonce なし
Nonce を必要としないアプリケーションの場合、CSP ヘッダーを next.config.js ファイルに直接設定できます:
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
]
},
}サブリソース完全性(実験段階)
Nonce の代替案として、Next.js はサブリソース完全性(SRI)を使用したハッシュベースの CSP に対する実験的なサポートを提供しています。このアプローチにより、静的生成を維持しながら、厳密な CSP を保つことができます。
補足:この機能は実験段階であり、App Router アプリケーションで webpack バンドラを使用する場合にのみ利用可能です。
SRI の動作方法
Nonce を使用する代わりに、SRI はビルド時に JavaScript ファイルの暗号化ハッシュを生成します。これらのハッシュは、スクリプトタグに integrity 属性として追加され、ブラウザがファイルが転送中に変更されていないことを確認できます。
SRI を有効にする
next.config.js に実験的な SRI 設定を追加します:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
sri: {
algorithm: 'sha256', // または 'sha384' または 'sha512'
},
},
}
module.exports = nextConfigSRI を使用した CSP 設定
SRI が有効な場合、既存の CSP ポリシーを引き続き使用できます。SRI はアセットに integrity 属性を追加することで独立して機能します:
補足:動的レンダリング シナリオでは、必要に応じて proxy で nonce を引き続き生成できます。SRI 完全性属性と nonce ベースの CSP アプローチの両方を組み合わせることができます。
const cspHeader = `
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
module.exports = {
experimental: {
sri: {
algorithm: 'sha256',
},
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
]
},
}Nonce に対する SRI の利点
- 静的生成:ページを静的に生成してキャッシュできます
- CDN 互換性:静的ページは CDN キャッシング で機能します
- より優れたパフォーマンス:各リクエストにはサーバーサイドレンダリング が必要ありません
- ビルド時のセキュリティ:ハッシュはビルド時に生成され、完全性を保証します
SRI の制限事項
- 実験段階:機能は変更または削除される可能性があります
- Webpack のみ:Turbopack では利用できません
- App Router のみ:Pages Router ではサポートされていません
- ビルド時のみ:動的に生成されたスクリプトを処理できません
開発環境と本番環境での注意事項
CSP の実装は開発環境と本番環境で異なります。
開発環境
開発環境では、追加のデバッグ情報を提供する API をサポートするために 'unsafe-eval' を有効にする必要があります:
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ''};
style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`};
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
// proxy の実装の残りの部分
}本番環境へのデプロイ
本番環境での一般的な問題:
- Nonce が適用されない:proxy がすべての必要なルートで実行されていることを確認してください
- 静的アセットがブロックされる:CSP が Next.js の静的アセットを許可していることを確認してください
- サードパーティスクリプト:必要なドメインを CSP ポリシーに追加してください
トラブルシューティング
サードパーティスクリプト
CSP でサードパーティスクリプトを使用する場合:
import { GoogleTagManager } from '@next/third-parties/google'
import { headers } from 'next/headers'
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const nonce = (await headers()).get('x-nonce')
return (
<html lang="en">
<body>
{children}
<GoogleTagManager gtmId="GTM-XYZ" nonce={nonce} />
</body>
</html>
)
}CSP を更新して、サードパーティドメインを許可します:
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' data: https://www.google-analytics.com;
`一般的な CSP 違反
- インラインスタイル:nonce をサポートする CSS-in-JS ライブラリを使用するか、スタイルを外部ファイルに移動してください
- 動的インポート:動的インポートが script-src ポリシーで許可されていることを確認してください
- WebAssembly:WebAssembly を使用する場合、
'wasm-unsafe-eval'を追加してください - Service Worker:Service Worker スクリプト用に適切なポリシーを追加してください
バージョン履歴
| バージョン | 変更内容 |
|---|---|
v14.0.0 | ハッシュベースの CSP に対する実験的 SRI サポート追加 |
v13.4.20 | 適切な nonce 処理と CSP ヘッダー解析に推奨 |