Content Security Policy

Content Security Policy (CSP) (opens in a new tab)는 Next.js 애플리케이션을 크로스 사이트 스크립팅(XSS), 클릭재킹 및 기타 코드 주입 공격과 같은 다양한 보안 위협으로부터 보호하는 데 중요합니다.

CSP를 사용하여 개발자는 콘텐츠 소스, 스크립트, 스타일시트, 이미지, 폰트, 객체, 미디어(오디오, 비디오), iframes 등을 허용할 출처를 지정할 수 있습니다.

Examples

Nonces

nonce (opens in a new tab)는 일회용으로 생성된 고유한 임의 문자열입니다. 이는 CSP와 함께 사용되어 특정 인라인 스크립트나 스타일을 선택적으로 실행할 수 있도록 하여 엄격한 CSP 지시를 우회할 수 있습니다.

Why use a nonce?

CSP는 악성 스크립트를 차단하도록 설계되었지만, 인라인 스크립트가 필요한 합법적인 시나리오가 존재합니다. 이러한 경우 nonce를 사용하여 올바른 nonce를 가진 스크립트가 실행될 수 있도록 합니다.

Adding a nonce with Middleware

Middleware를 사용하여 페이지가 렌더링되기 전에 헤더를 추가하고 nonce를 생성할 수 있습니다.

페이지가 조회될 때마다 새로운 nonce가 생성되어야 합니다. 이는 nonce를 추가하려면 동적 렌더링을 사용해야 함을 의미합니다.

예시:

middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(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
}
middleware.js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  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
}

기본적으로 Middleware는 모든 요청에서 실행됩니다. matcher를 사용하여 특정 경로에서만 Middleware를 실행하도록 필터링할 수 있습니다.

next/link의 사전 로드 및 CSP 헤더가 필요 없는 정적 자산의 매칭을 무시하는 것이 좋습니다.

middleware.ts
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' },
      ],
    },
  ],
}
middleware.js
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' },
      ],
    },
  ],
}

Reading the nonce

이제 headers를 사용하여 서버 컴포넌트에서 nonce를 읽을 수 있습니다:

app/page.tsx
import { headers } from 'next/headers'
import Script from 'next/script'
 
export default function Page() {
  const nonce = headers().get('x-nonce')
 
  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}
app/page.jsx
import { headers } from 'next/headers'
import Script from 'next/script'
 
export default function Page() {
  const nonce = headers().get('x-nonce')
 
  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}

Without Nonces

nonce가 필요 없는 애플리케이션의 경우 next.config.js 파일에서 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, ''),
          },
        ],
      },
    ]
  },
}

Version History

nonce를 올바르게 처리하고 적용하려면 Next.js의 v13.4.20+ 버전을 사용하는 것이 좋습니다.