SecuCore · 가이드

Next.js·Vercel 보안 헤더 설정 — 실제 7개 사이트 적용 기록

보안 헤더가 뭔지는 공식 문서가 이미 잘 설명한다. 정작 안 나와 있는 건 넣으면 뭐가 깨지는가 다. 아래는 운영 중인 서비스 7곳에 실제로 넣고 다시 진단한 기록이다. 점수는 SecuCore 기준이고, 절대적인 안전 상태를 뜻하지 않는다.

진단 → 적용 → 재진단 실측

사이트적용 전적용 후방법비고
chalguide.vercel.appD (57)B (87)vercel.jsonNext 정적 export
hc-gbus.vercel.appD (57)B (87)next.config 머지기존 sw.js 헤더 보존
getcalcnest.comD (57)B (87)vercel.json정적 사이트
tiermaster.vercel.appF (48)C (78)next.configCapacitor WebView
www.nyanquest.comF (48)D (69)next.config프레이밍 헤더 의도적 제외
moonlitoracle.comC (78)C (78)기적용CSP enforce 전환만 남음
secucore.vercel.appA (96)next.config자기 스캔 통과

7곳 모두 코드 취약점(XSS·eval 등)은 0건이었다. 부족했던 것은 전부 응답 헤더 설정이었다. 즉 대부분의 사이트에서 이 작업은 코드를 고치는 일이 아니라 설정 한 곳을 채우는 일이다.

표준 헤더 6종

헤더무엇을 막나
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadHTTPS 를 강제하고 프로토콜 다운그레이드를 막는다.
Content-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline'; …허용하지 않은 출처의 스크립트·리소스 실행을 막는다. XSS 피해 범위를 줄이는 마지막 방어선.
X-Frame-OptionsSAMEORIGIN (또는 DENY)다른 사이트가 내 페이지를 iframe 에 넣는 것을 막는다. 클릭재킹 방어.
X-Content-Type-Optionsnosniff브라우저가 Content-Type 을 무시하고 내용을 추측하는 것을 막는다.
Referrer-Policystrict-origin-when-cross-origin외부로 나갈 때 전체 URL 대신 출처만 넘긴다. 쿼리스트링에 담긴 정보 유출 방지.
Permissions-Policycamera=(), microphone=(), geolocation=()쓰지 않는 브라우저 기능을 아예 꺼둔다. 서드파티 스크립트가 몰래 요청하는 것을 차단.

복붙 설정

Next.js (App Router · Pages Router 공통)

// next.config.mjs
const csp = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-inline'",
  "style-src 'self' 'unsafe-inline'",
  "img-src 'self' data:",
  "connect-src 'self'",
  "object-src 'none'",
  "base-uri 'self'",
  "form-action 'self'",
  "frame-ancestors 'none'",
  "upgrade-insecure-requests",
].join('; ');

const nextConfig = {
  async headers() {
    return [{
      source: '/(.*)',
      headers: [
        { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
        { key: 'Content-Security-Policy', value: csp },
        { key: 'X-Frame-Options', value: 'DENY' },
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
      ],
    }];
  },
};

export default nextConfig;

정적 사이트 (Vercel)

// vercel.json  — 정적 사이트(빌드 산출물만 올리는 경우)
{
  "headers": [{
    "source": "/(.*)",
    "headers": [
      { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
      { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
      { "key": "X-Content-Type-Options", "value": "nosniff" },
      { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
      { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
    ]
  }]
}

넣으면 깨지는 경우

WebView·미니앱은 프레이밍 헤더를 빼야 한다

토스 미니앱처럼 다른 앱이 내 페이지를 감싸서 띄우는 구조라면 X-Frame-Options / frame-ancestors 를 그대로 넣는 순간 화면이 안 뜬다. 위 표에서 nyanquest 만 F(48) → D(69) 로 덜 오른 이유가 이것이다. 점수를 포기하고 동작을 지킨 것이고, 그게 맞는 선택이다.

외부 스크립트가 있으면 CSP 가 광고·분석을 죽인다

AdSense·GA·픽셀을 쓰는 사이트에 default-src 'self' 만 넣으면 그 스크립트들이 전부 차단된다. 매출이 광고에서 나오는 사이트라면 조용히 수입이 끊긴다. 먼저 Content-Security-Policy-Report-Only 로 며칠 돌려 위반 리포트를 보고, 실제로 쓰는 도메인만 script-src·connect-src 에 추가한 뒤 enforce 로 바꾼다.

HSTS preload 는 되돌리기 어렵다

preload 목록에 올라가면 브라우저가 하드코딩으로 HTTPS 만 쓴다. 서브도메인 중 하나라도 HTTPS 가 안 되면 그 서브도메인이 통째로 접속 불가가 된다. includeSubDomains 를 넣기 전에 모든 서브도메인이 HTTPS 인지 먼저 확인한다.

기존 헤더 설정이 있으면 덮어쓰지 말고 머지한다

PWA 서비스워커나 CORS 때문에 이미 headers() 를 쓰고 있는 프로젝트가 흔하다. 통째로 갈아끼우면 그쪽이 깨진다. 위 표의 hc-gbus 는 기존 sw.js 헤더를 보존하면서 머지한 사례다.

적용했으면 다시 재봐야 한다

헤더는 넣었다고 끝이 아니라 실제 응답에 실려 나가는지가 전부다. 리다이렉트 뒤 최종 페이지에만 안 붙거나, 프레임워크 기본값에 덮여 사라지는 경우가 흔하다. 배포 후 다시 진단해서 확인하는 편이 안전하다.

→ 내 사이트 1분 진단해보기 · 회원가입 없음

표의 점수·등급은 SecuCore 로 실제 진단·재진단한 값이다. 이 글은 AI 의 도움을 받아 작성했다. 보안 진단 결과는 참고 자료이며, 특정 시점의 응답을 기준으로 한 점검이지 모든 취약점의 부재를 보장하지 않는다.