SecuCore · 가이드
Next.js·Vercel 보안 헤더 설정 — 실제 7개 사이트 적용 기록
보안 헤더가 뭔지는 공식 문서가 이미 잘 설명한다. 정작 안 나와 있는 건 넣으면 뭐가 깨지는가 다. 아래는 운영 중인 서비스 7곳에 실제로 넣고 다시 진단한 기록이다. 점수는 SecuCore 기준이고, 절대적인 안전 상태를 뜻하지 않는다.
진단 → 적용 → 재진단 실측
| 사이트 | 적용 전 | 적용 후 | 방법 | 비고 |
|---|---|---|---|---|
| chalguide.vercel.app | D (57) | B (87) | vercel.json | Next 정적 export |
| hc-gbus.vercel.app | D (57) | B (87) | next.config 머지 | 기존 sw.js 헤더 보존 |
| getcalcnest.com | D (57) | B (87) | vercel.json | 정적 사이트 |
| tiermaster.vercel.app | F (48) | C (78) | next.config | Capacitor WebView |
| www.nyanquest.com | F (48) | D (69) | next.config | 프레이밍 헤더 의도적 제외 |
| moonlitoracle.com | C (78) | C (78) | 기적용 | CSP enforce 전환만 남음 |
| secucore.vercel.app | — | A (96) | next.config | 자기 스캔 통과 |
7곳 모두 코드 취약점(XSS·eval 등)은 0건이었다. 부족했던 것은 전부 응답 헤더 설정이었다. 즉 대부분의 사이트에서 이 작업은 코드를 고치는 일이 아니라 설정 한 곳을 채우는 일이다.
표준 헤더 6종
| 헤더 | 값 | 무엇을 막나 |
|---|---|---|
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | HTTPS 를 강제하고 프로토콜 다운그레이드를 막는다. |
| Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; … | 허용하지 않은 출처의 스크립트·리소스 실행을 막는다. XSS 피해 범위를 줄이는 마지막 방어선. |
| X-Frame-Options | SAMEORIGIN (또는 DENY) | 다른 사이트가 내 페이지를 iframe 에 넣는 것을 막는다. 클릭재킹 방어. |
| X-Content-Type-Options | nosniff | 브라우저가 Content-Type 을 무시하고 내용을 추측하는 것을 막는다. |
| Referrer-Policy | strict-origin-when-cross-origin | 외부로 나갈 때 전체 URL 대신 출처만 넘긴다. 쿼리스트링에 담긴 정보 유출 방지. |
| Permissions-Policy | camera=(), 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 의 도움을 받아 작성했다. 보안 진단 결과는 참고 자료이며, 특정 시점의 응답을 기준으로 한 점검이지 모든 취약점의 부재를 보장하지 않는다.