Next.jsアプリケーションのContent Security Policy (CSP) の設定方法
Content Security Policy (CSP) は、クロスサイトスクリプティング(XSS)、クリックジャッキング、その他のコードインジェクション攻撃からNext.jsアプリケーションを保護するために重要です。
CSPを使用することで、開発者はコンテンツソース、スクリプト、スタイルシート、画像、フォント、オブジェクト、メディア(オーディオ、ビデオ)、iframeなど、許可されるオリジンを指定できます。
Nonce
A nonce は、一度だけ使用される一意のランダムな文字列です。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;
`
// Replace newline characters and spaces
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を実行するようにフィルタリングできます。
prefetch(next/linkから)とCSPヘッダーを必要としない静的アセットは無視することをお勧めします。
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
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を生成: プロキシはリクエストごとに一意のNonceを生成し、それを
Content-Security-Policyヘッダーに追加し、カスタムx-nonceヘッダーにも設定します。 - Next.jsがNonceを抽出: レンダリング中、Next.jsは
Content-Security-Policyヘッダーを解析し、'nonce-{value}'パターンを使用してNonceを抽出します。 - Nonceが自動的に適用される: Next.jsはNonceを以下にアタッチします。
- フレームワークスクリプト(React、Next.jsランタイム)
- ページ固有のJavaScriptバンドル
- Next.jsによって生成されたインラインスタイルとスクリプト
nonceプロップを使用した<Script>コンポーネント
この自動的な動作により、各タグに手動でNonceを追加する必要はありません。
動的レンダリングを強制する
Nonceを使用している場合、ページを明示的に動的レンダリングにオプトインする必要がある場合があります。
import { connection } from 'next/server'
export default async function Page() {
// wait for an incoming request to render this page
await connection()
// Your page content
}Nonceを読み取る
Nonceは、Server Componentでheadersを使用して読み取ることができます。
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://#/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}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, ''),
},
],
},
]
},
}Subresource Integrity (実験的)
Nonceの代替として、Next.jsはハッシュベースのCSPに対する実験的なサポートとしてSubresource Integrity (SRI) を提供します。このアプローチにより、厳格なCSPを維持しながら静的生成を維持できます。
重要: この機能は実験的であり、App Routerアプリケーションのwebpackバンドラーでのみ利用可能です。
SRIの仕組み
Nonceの代わりに、SRIはビルド時にJavaScriptファイルの暗号学的ハッシュを生成します。これらのハッシュはスクリプトタグにintegrity属性として追加され、ブラウザは転送中にファイルが変更されていないことを確認できます。
SRIを有効にする
next.config.jsに実験的なSRI設定を追加します。
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
sri: {
algorithm: 'sha256', // or 'sha384' or '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;
`
// Rest of proxy implementation
}本番環境へのデプロイ
本番環境でよくある問題
- Nonceが適用されていない: プロキシが必要なすべてのルートで実行されていることを確認してください。
- 静的アセットがブロックされる: 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://#;
connect-src 'self' https://#;
img-src 'self' data: https://#;
`一般的なCSP違反
- インラインスタイル: NonceをサポートするCSS-in-JSライブラリを使用するか、スタイルを外部ファイルに移動します。
- 動的インポート: スクリプトソースポリシーで動的インポートが許可されていることを確認してください。
- WebAssembly: WebAssemblyを使用している場合は、
'wasm-unsafe-eval'を追加します。 - サービスワーカー: サービスワーカー スクリプトに適切なポリシーを追加します。
バージョン履歴
| バージョン | 変更履歴 |
|---|---|
v14.0.0 | ハッシュベースのCSPのための実験的なSRIサポートを追加しました。 |
v13.4.20 | 適切なNonce処理とCSPヘッダー解析に推奨されます。 |
役に立ちましたか?