next.jsでcspを設定したので、CSPについてまとめておきます。
CSPとは
Content-Security-Policyの略
(ブラウザ側での)コンテンツ読み込みを制限して、XSSやデータインジェクションなどのセキュリティリスクを軽減する仕組みのこと。
詳細
具体的に言うと、
- ブラウザ側がリクエスト
⇩ - リクエストされたサーバー側は、
Content-Security-Policy: default-src 'self';
のような形でレスポンスヘッダーとして返す。
⇩ - 受け取ったブラウザは、policyに合致するもののみ読み込みを許可し、しないものはブロックする。
ですので、サーバー側(Next.js)が実装することは、CSPヘッダーをレスポンスヘッダーに設定してあげることになります。
実装例
公式の実装例に従って実装しました。
"next": "14.2.11",
"react": "18.3.1",
"typescript": "5.6.3"
※App router使用
nonceを使うか使わないかの2通りの方法があり、それぞれ紹介します。
①nonceを使う
ナンス(nonce)とは「number used once」の略で、一回限りの一意な値です。
インラインスクリプト等にnonce属性を付与し、CSPヘッダーに設定されたnonce値と一致するもののみ許可する仕組みです。
下記の場合は、許可されます。
<script nonce="hogehoge">
Content-Security-Policy: script-src 'nonce-hogehoge'
middleware
具体的な実装例を示します。
nonceを使う場合は、middlewareを使います。
middlewareの詳細な説明は割愛しますが、役割としては、サーバーとクライアントの狭間の役割をしています。
middlewareを使うことで、
- ページがレンダリングされる前にヘッダーを追加し、nonceを生成することができる
- ページが表示されるたびに、新しいnonceを生成することができる
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// nonce値の生成
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()
// (SSRする前に)リクエストヘッダーに`x-nonce`を追加
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
// レスポンスヘッダーにCSPを追加
// nonceを使わない場合は、ここのみでもOK
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
return response
}
page側(SSR)
次に、middlewareが実行された後にSSRします。
import { headers } from 'next/headers'
import Script from 'next/script'
export default function Page() {
// middlewareで追加されたx-nonceヘッダー値を取得
const nonce = headers().get('x-nonce')
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
// インラインスクリプトのnonce属性にnonce値を設定
nonce={nonce}
/>
)
}
②nonceを使わない
nonceを使わない場合は、SSRする前に処理を差し込む必要がないので、middlewareは不要です。
next.configに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, ''),
},
],
},
]
},
}