Next.js 15でContent Security Policy (CSP) を実装する際の課題と解決策
Next.js を使用したアプリケーション開発でセキュリティ対策を強化するために、Content Security Policy(以下:CSP)を導入しました。
本記事では、CSP の基礎知識から Next.js での実装方法、実際に遭遇した課題と解決策について記録しています。
https://github.com/kntks/blog-code/tree/main/2025/09/nextjs-csp-demo
バージョン | |
---|---|
macOS | Sequoia 15.6 |
Node.js | 24.6.0 |
Next.js | 15.5.3 |
CSP とは
Section titled “CSP とは”CSP は、クロスサイトスクリプティングのようなコンテンツインジェクション攻撃のリスクを軽減するセキュリティレイヤーです。アプリケーションの実行権限を制限することで、セキュリティを向上させます。
以下の引用に書かれているように、CSP は許可するリソースを設定します。
CSP を使用して、文書が読み込むことを許可されるリソースを制御することができます。これは主に、クロスサイトスクリプティング (XSS) 攻撃からの保護に使用されます。
引用: コンテンツセキュリティポリシー (CSP) - MDN
CSP の主要な目的
Section titled “CSP の主要な目的”CSP は、開発者が以下の要素をきめ細かく制御することで、コンテンツインジェクション攻撃のリスクを軽減します。
制御可能な要素
Section titled “制御可能な要素”- リソースの読み込み制御
特定のドキュメントや Worker に代わって要求できるリソース - インライン・スクリプトの実行制御
HTML 内に直接記述されたスクリプトの実行可否 - 動的コード実行の制御
eval() および類似のコンストラクトによる動的実行 - インラインスタイルの制御
HTML 内に直接記述されたスタイルの適用可否
参考:1.2. Goals - Content Security Policy Level 3
ポリシーの適用対象
Section titled “ポリシーの適用対象”CSP は、HTML などのドキュメントや Worker に設定・適用できます。
A policy defines allowed and restricted behaviors, and may be applied to a Document, WorkerGlobalScope, or WorkletGlobalScope.
ポリシーは、許可される動作と制限される動作を定義し、Document、WorkerGlobalScope、または WorkletGlobalScope に適用することができます。
引用:2.2. Policies - Content Security Policy Level 3
Next.js での CSP 実装
Section titled “Next.js での CSP 実装”このセクションでは、Next.js アプリケーションに CSP を実装する具体的な方法と考慮すべき点について説明します。
Next.js が提供する CSP 設定方法
Section titled “Next.js が提供する CSP 設定方法”Next.js では以下の3つの方法を提供しています。今回は nonce ベースの CSP を採用します。
- nonce ベースの CSP
middleware.ts で Nonce を設定する方法 - Subresource Integrity (SRI) を使用したハッシュベースの CSP
スクリプトのハッシュ値を使用してセキュリティを確保する方法 - ランダムな文字列を使用しない CSP
next.config.ts で header を設定する方法
Nonce ベースの CSP
Section titled “Nonce ベースの CSP”nonce ベースの CSP を使用する場合、Next.js アプリケーションは middleware.ts を使用して nonce を生成し、CSP ヘッダーに含める必要があります。
middleware.ts の実装例
Section titled “middleware.ts の実装例”import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) { // /secure パスのみCSPヘッダーを適用 if (request.nextUrl.pathname.includes('/secure')) { 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()
// x-nonce ヘッダーを追加することで、Next.js は // 自動で script タグに nonce を設定する const requestHeaders = new Headers(request.headers) requestHeaders.set('x-nonce', nonce)
// Next.js Server からのレスポンスに CSP ヘッダーを追加し // ブラウザに送信 const response = NextResponse.next({ request: { headers: requestHeaders, }, }) response.headers.set( 'Content-Security-Policy', contentSecurityPolicyHeaderValue )
return response }
return NextResponse.next()}
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) */ '/((?!api|_next/static|_next/image|favicon.ico).*)', ],};
layout.tsx の設定例
Section titled “layout.tsx の設定例”layout.tsx の RootLayout を async component にし、await connection()
を呼び出すようにします。そうすることで、全ページを Dynamic Rendering にしています。
export default async function RootLayout({ children,}: Readonly<{ children: React.ReactNode;}>) {
/** * force dynamic rendering for Content Security Policy * @see https://nextjs.org/docs/app/guides/content-security-policy#forcing-dynamic-rendering */ await connection();
return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} > {children} </body> </html> );}
実装時の主要な課題
Section titled “実装時の主要な課題”1. 全ページへの CSP 適用の重要性
Section titled “1. 全ページへの CSP 適用の重要性”ページ遷移によって受け取るリクエストは HTML ではなく、React Server Component Payload(通称:RSC Payload)のみの場合があります。
問題が発生する条件:
<Link>
コンポーネントやuseRouter
を使用したクライアントサイド遷移の場合- 従来のフルページロード(ブラウザのアドレスバーに直接 URL を入力や
<a>
タグを使用した場合など)では問題は発生しません
Next.js のクライアントサイド遷移の仕組み:
Next.js は <Link>
コンポーネントでクライアントサイド遷移を実行します。公式ドキュメントによると以下のように説明されています。
To further improve the navigation experience, Next.js performs a client-side transition with the component.
Client-side transitions Traditionally, navigation to a server-rendered page triggers a full page load. This clears state, resets scroll position, and blocks interactivity.
Next.js avoids this with client-side transitions using the component. Instead of reloading the page, it updates the content dynamically by:
- Keeping any shared layouts and UI.
- Replacing the current page with the prefetched loading state or a new page if available.
引用:Linking and Navigating - Next.js
Good to know: During the initial navigation, the browser fetches the HTML, JavaScript, and React Server Components (RSC) Payload.
For subsequent navigations, the browser will fetch the RSC Payload for Server Components and JS bundle for Client Components.
CSP が適用されない理由:
CSP は HTML ドキュメントのヘッダーで定義されるセキュリティポリシーです。クライアントサイド遷移では HTML ドキュメント全体を再読み込みしないため、Next.js は RSC Payload のみを取得してコンテンツを動的に更新します。そのため、遷移先ページの CSP ヘッダーが読み込まれず、遷移元ページの CSP 設定が維持される仕組みになっています。
対策: すべてのページに CSP を適用する必要があります。特定のページにのみ CSP を設定した場合、クライアントサイド遷移時に期待通りの CSP が適用されません。
具体例:
CSP が設定されていないページ A から CSP が設定されているページ B へ <Link>
コンポーネントで遷移した場合、ページ B では CSP が適用されません。これは、クライアントサイド遷移により HTML ドキュメント全体ではなく RSC Payload のみが返されるため、ページ A の CSP 設定(または未設定状態)がそのまま維持されるためです。
<Link>
コンポーネントと useRouter
で動作確認してみたところ、ページ B に遷移したところで _rsc=
の query parameter がついた RSC Payload が返され、CSP が適用されないことを確認しました。
2. UI ライブラリとの互換性問題
Section titled “2. UI ライブラリとの互換性問題”CSP の style-src
ディレクティブを使用する際の課題です。
問題の詳細:
- UI コンポーネントライブラリが動的にスタイルを生成する場合がある
style-src 'nonce-xxxx'
設定により、動的スタイルが適用されなくなる可能性
具体例: shadcn/ui の Input OTP を使用した際に、OTP の入力ができなくなりました。
現状と対策:
- 2025年9月現在、nonce 対応の issue が上がっているが未対応です
参考:https://github.com/guilhermerodz/input-otp/issues/48 - Subresource Integrity (SRI) の活用も検討可能です(Next.js 15.5.0 では experimental)
style-src 'unsafe-inline'
を設定することで動的スタイルを許可できます。ただし、セキュリティリスクが高まるため注意が必要です。
その他の学習内容
Section titled “その他の学習内容”ここでは、CSP を学習する過程で学んだ内容を記録します。
ブラウザによる nonce 値の比較
Section titled “ブラウザによる nonce 値の比較”ブラウザは、HTML の script タグにある nonce の値と
<script src="/_next/static/chunks/f8b0f_next_dist_client_5511b396._.js" async="" nonce="M2RkMTUxNTktOGU1Ni00MDg4LWE2ZGItNTY2NTdmNTJhMTdm"></script>
HTML のレスポンスヘッダーにある Content-Security-Policy
の nonce を比較しています。
Content-Security-Policy: script-src 'nonce-M2RkMTUxNTktOGU1Ni00MDg4LWE2ZGItNTY2NTdmNTJhMTdm'
参考:ノンス - MDN
innerHTML とブラウザ
Section titled “innerHTML とブラウザ”innerHTML に対して script タグを使用しても JavaScript は実行されません。
When inserted using the document.write() method, script elements usually execute (typically blocking further script execution or HTML parsing). When inserted using the innerHTML and outerHTML attributes, they do not execute at all.
document.write() メソッドを使って挿入された場合、スクリプト要素は通常実行されます(通常、それ以降のスクリプト実行や HTML 解析はブロックされます)。innerHTML や outerHTML 属性を使って挿入された場合、それらは全く実行されません。
引用:4.12.1 The script element - HTML Living Standard
default-src ‘self’
Section titled “default-src ‘self’”-src
で終わるディレクティブかつ未指定の場合、default-src
の値が使用されます。そのため、form-action
のようなディレクティブは default-src
の値を継承しません。
デフォルトをオーバーライドするには、default-src ディレクティブを指定します。このディレクティブは、-src で終わる未指定のディレクティブのデフォルトを定義します
重要なポイント
Section titled “重要なポイント”- CSP はクロスサイトスクリプティング攻撃のリスクを軽減するセキュリティレイヤー
- Next.js では nonce ベース、ハッシュベース、固定値の3つの CSP 設定方法を提供
- nonce ベースの CSP を使用する場合は Dynamic Rendering が必須
- UI ライブラリとの互換性に注意が必要
- 全ページに CSP を適用することが重要
CSP の導入により、アプリケーションのセキュリティを大幅に向上させることができますが、実装時にはさまざまな考慮点があることを理解して進めることが大切だと学びました。
- How to set a Content Security Policy (CSP) for your Next.js application - Next.js
- Content-Security-Policy (CSP) - MDN
- コンテンツセキュリティポリシー (CSP) - MDN
- 厳格なコンテンツ セキュリティ ポリシー(CSP)を使用してクロスサイト スクリプティング(XSS)を軽減する - web.dev
- コンテンツ セキュリティ ポリシー - web.dev
- Content Security Policy Cheat Sheet - OWASP
- 安全なウェブサイトの作り方 - 1.5 クロスサイト・スクリプティング - IPA
- CSP の XSS 攻撃に対する効果を確認する - Lighthouse
- 複雑な環境における Content-Security-Policy - Okta
- Content Security Policy Level 3 - W3C
- Content Security Policy Level 3におけるXSS対策
- CSP: default-src - MDN