ReactNextCentral

콘텐츠 보안 정책

Published on
Next.js 애플리케이션에 대한 콘텐츠 보안 정책(CSP)을 설정하는 방법을 알아봅니다.
Table of Contents

콘텐츠 보안 정책 (CSP)은 크로스 사이트 스크립팅(XSS), 클릭 재킹, 기타 코드 주입 공격과 같은 다양한 보안 위협으로부터 Next.js 애플리케이션을 보호하는 데 중요합니다.

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

엄격한 CSP 예제: 링크

Nonces (난스)

난스는 한 번만 사용하기 위해 생성된 고유하고 무작위 문자열입니다. 이것은 엄격한 CSP 지시문을 우회하여 특정 인라인 스크립트나 스타일을 실행하도록 허용하기 위해 CSP와 함께 사용됩니다.

왜 난스를 사용해야 할까요?

CSP는 악의적인 스크립트를 차단하도록 설계되었지만, 인라인 스크립트가 필요한 합당한 상황이 있습니다. 이러한 경우 난스는 올바른 난스를 가진 스크립트가 실행될 수 있도록 허용하는 방법을 제공합니다.

미들웨어를 이용한 난스 추가하기

미들웨어는 페이지가 렌더링되기 전에 헤더를 추가하고 난스를 생성할 수 있게 해줍니다. 페이지가 조회될 때마다 새로운 난스가 생성되어야 합니다. 이것은 난스를 추가하기 위해 동적 렌더링을 사용해야 함을 의미합니다.

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';
    block-all-mixed-content;
    upgrade-insecure-requests;
`

  const requestHeaders = new Headers()
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set(
    'Content-Security-Policy',
    // 줄바꿈 문자와 공백을 대체
    cspHeader.replace(/\s{2,}/g, ' ').trim()
  )

  return NextResponse.next({
    headers: requestHeaders,
    request: {
      headers: requestHeaders,
    },
  })
}
JavaScript
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';
    block-all-mixed-content;
    upgrade-insecure-requests;
`

  const requestHeaders = new Headers()
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set(
    'Content-Security-Policy',
    // 줄바꿈 문자와 공백을 대체
    cspHeader.replace(/\s{2,}/g, ' ').trim()
  )

  return NextResponse.next({
    headers: requestHeaders,
    request: {
      headers: requestHeaders,
    },
  })
}

기본적으로 미들웨어는 모든 요청에서 실행됩니다. matcher를 사용하여 특정 경로에서 미들웨어가 실행되도록 필터링할 수 있습니다. 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' },
      ],
    },
  ],
}
JavaScript
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' },
      ],
    },
  ],
}

nonce 읽기

이제 headers를 사용하여 Server Component에서 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}
    />
  )
}
JavaScript
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}
    />
  )
}