Content Security Policy (CSP) を Next.js アプリケーションに設定する方法
Content Security Policy (CSP) は、クロスサイトスクリプティング (XSS)、クリックジャッキング、その他のコードインジェクション攻撃などの様々なセキュリティ脅威から Next.js アプリケーションを保護するために重要です。
CSP を使用することで、開発者はコンテンツソース、スクリプト、スタイルシート、画像、フォント、オブジェクト、メディア(オーディオ、ビデオ)、iframe など、許可されるオリジンを指定できます。
例
Nonce(ナンス)
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 を実行するようにフィルタリングできます。
next/link からのプリフェッチ(prefetch)や 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 を生成: Proxy がリクエストごとに一意の 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 は、getServerSideProps を使用してページに提供できます。
import Script from 'next/script'
import type { GetServerSideProps } from 'next'
export default function Page({ nonce }) {
return (
<Script
src="https://#/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const nonce = req.headers['x-nonce']
return { props: { nonce } }
}Pages Router アプリケーションでは、_document.tsx で Nonce にアクセスすることもできます。
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
DocumentInitialProps,
} from 'next/document'
interface ExtendedDocumentProps extends DocumentInitialProps {
nonce?: string
}
class MyDocument extends Document<ExtendedDocumentProps> {
static async getInitialProps(
ctx: DocumentContext
): Promise<ExtendedDocumentProps> {
const initialProps = await Document.getInitialProps(ctx)
const nonce = ctx.req?.headers?.['x-nonce'] as string | undefined
return {
...initialProps,
nonce,
}
}
render() {
const { nonce } = this.props
return (
<Html lang="en">
<Head nonce={nonce} />
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
)
}
}
export default MyDocumentCSP における静的レンダリングと動的レンダリング
Nonce の使用は、Next.js アプリケーションのレンダリング方法に重要な影響を与えます。
動的レンダリングの要件
CSP で Nonce を使用する場合、すべてのページを動的レンダリングする必要があります。これは意味します
- ページは正常にビルドされますが、動的レンダリング用に正しく設定されていない場合は実行時エラーが発生する可能性があります。
- 各リクエストは、新しい Nonce を持つ新しいページを生成します。
- 静的最適化とインクリメンタル静的再生(ISR)は無効になります。
- CDN によるページのキャッシュは、追加の設定なしではできません。
- 部分プリレンダリング(PPR)は互換性がありません。Nonce ベースの CSP では、静的シェルスクリプトは Nonce にアクセスできないためです。
パフォーマンスへの影響
静的レンダリングから動的レンダリングへの移行は、パフォーマンスに影響します。
- 初期ページロードが遅くなる: 各リクエストでページを生成する必要があります。
- サーバー負荷の増加: すべてのリクエストでサーバーサイドレンダリングが必要です。
- CDN キャッシュなし: 動的ページはデフォルトではエッジでキャッシュできません。
- ホスティングコストの増加: 動的レンダリングにはより多くのサーバーリソースが必要です。
Nonce を使用するタイミング
Nonce の使用を検討するのは、以下のような場合です。
'unsafe-inline'を禁止する厳格なセキュリティ要件がある場合- 機密データを扱うアプリケーションの場合
- 特定のインラインスクリプトを許可し、他のスクリプトをブロックする必要がある場合
- コンプライアンス要件で厳格な CSP が義務付けられている場合
Nonce を使用しない場合
Nonce が不要なアプリケーションでは、next.config.js ファイルで直接 CSP ヘッダーを設定できます。
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, ''),
},
],
},
]
},
}開発環境と本番環境の考慮事項
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 が適用されない: Proxy が必要なすべてのルートで実行されていることを確認してください。
- 静的アセットがブロックされる: CSP が Next.js の静的アセットを許可していることを確認してください。
- サードパーティスクリプト: CSP ポリシーにドメインを追加してください。
トラブルシューティング
サードパーティスクリプト
CSP でサードパーティスクリプトを使用する場合は、必要なドメインを追加し、Nonce を渡してください。
import type { AppProps } from 'next/app'
import Script from 'next/script'
export default function App({ Component, pageProps }: AppProps) {
const nonce = pageProps.nonce
return (
<>
<Component {...pageProps} />
<Script
src="https://#/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
)
}サードパーティドメインを許可するように 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 ライブラリを使用するか、スタイルを外部ファイルに移動してください。
- 動的インポート: スクリプトソース(script-src)ポリシーで動的インポートが許可されていることを確認してください。
- WebAssembly: WebAssembly を使用している場合は、
'wasm-unsafe-eval'を追加してください。 - サービスワーカー: サービスワーカー スクリプトの適切なポリシーを追加してください。
バージョン履歴
| バージョン | 変更履歴 |
|---|---|
v14.0.0 | ハッシュベース CSP のための実験的な SRI サポートが追加されました。 |
v13.4.20 | 適切な Nonce 処理と CSP ヘッダー解析に推奨されます。 |
役に立ちましたか?