OneWebDesk

필수 보안 헤더 설정 가이드(CSP·HSTS·X-Frame-Options)

꼭 설정해야 할 보안 헤더와 각 헤더의 역할·권장 값·흔한 실수.

HTTPS만 켜면 사이트가 안전하다고 생각하기 쉽지만, 실제로는 응답 헤더 몇 줄을 추가해야 비로소 클릭재킹· 크로스사이트 스크립팅(XSS)·MIME 스니핑·프로토콜 다운그레이드 같은 흔한 공격을 막을 수 있습니다. 이런 역할을 하는 것이 HTTP 보안 헤더입니다. 서버가 페이지를 내려줄 때 함께 보내는 지시문으로, 브라우저에게 “이 페이지는 이렇게 다뤄라”라고 알려 줍니다.

이 가이드는 실무에서 꼭 설정해야 할 핵심 보안 헤더 6종을 하나씩 짚고, 각 헤더가 무엇을 막는지, 권장 값은 무엇인지, 그리고 가장 흔한 실수까지 정리합니다. 마지막에는 현재 사이트에 어떤 헤더가 빠져 있는지 확인하는 방법도 안내합니다. 한 번 제대로 설정해 두면 보안 등급 평가에서 곧바로 점수가 오릅니다.

Content-Security-Policy (CSP) — XSS의 1차 방어선

CSP는 페이지가 어떤 출처의 스크립트·스타일·이미지·폰트를 불러올 수 있는지를 화이트리스트로 제한합니다. 공격자가 본문에 악성 <script>를 주입하더라도, 그 출처가 정책에 없으면 브라우저가 실행 자체를 거부합니다. 즉 XSS 취약점이 있더라도 실제 피해로 이어지는 것을 한 단계 더 막아 주는 가장 강력한 헤더입니다. 인라인 스크립트·인라인 스타일·eval() 차단, 폼 전송 대상 제한 (form-action), iframe 삽입 제한(frame-ancestors)까지 한 헤더로 통제할 수 있습니다.

다만 CSP는 가장 강력한 만큼 가장 깨지기 쉬운 헤더이기도 합니다. 정책이 너무 좁으면 정상 스크립트까지 막혀 사이트가 멈추고, 너무 넓으면 보안 효과가 사라집니다. 처음에는Content-Security-Policy-Report-Only로 보고만 받으며 위반 항목을 관찰한 뒤, 정책을 다듬어 정식 적용하는 것이 안전합니다. 사이트 구조에 맞는 정책 초안은 CSP 생성기로 만들어 시작하면 시행착오를 크게 줄일 수 있습니다.

Strict-Transport-Security (HSTS) — HTTPS 강제

사용자가 http://로 접속하거나 주소창에 프로토콜 없이 도메인만 입력하면, 첫 요청은 평문 HTTP로 나갑니다. 이 짧은 순간을 노려 중간자(MITM)가 트래픽을 가로채 다운그레이드시킬 수 있습니다. HSTS는 한 번 방문한 브라우저에게 “앞으로 이 도메인은 무조건 HTTPS로만 접속하라”고 기억시켜 그 틈을 없앱니다.

  • max-age: 이 지시를 기억할 기간(초). 권장 값은 2년인 63072000입니다.
  • includeSubDomains: 모든 하위 도메인에도 적용. 하위 도메인이 전부 HTTPS일 때만 켜세요.
  • preload: 브라우저 내장 목록(HSTS preload list)에 등록해 첫 방문조차 HTTP를 거치지 않게 함. 등록은 사실상 되돌리기 어려우니 신중히.

권장 값: Strict-Transport-Security: max-age=63072000; includeSubDomains; preload. 단, 아직 HTTPS로 못 옮긴 하위 도메인이 하나라도 있다면 includeSubDomains는 빼야 그 도메인이 접속 불능이 되는 사고를 막을 수 있습니다.

클릭재킹·MIME·참조자·권한 헤더

나머지 헤더들은 한 줄씩이면 끝나지만 효과는 분명합니다.

  • X-Frame-Options / frame-ancestors (클릭재킹 방어): 공격자가 내 페이지를 투명한<iframe>에 얹어 사용자가 보이지 않는 버튼을 누르게 하는 공격을 막습니다.X-Frame-Options: DENY로 모든 iframe 삽입을 차단하거나, CSP의frame-ancestors 'self'로 같은 출처만 허용합니다. CSP의 frame-ancestors가 최신·우선 표준이며, 구형 브라우저 호환을 위해 둘 다 두는 경우가 많습니다.
  • X-Content-Type-Options: nosniff (MIME 스니핑 차단): 브라우저가 응답의Content-Type을 무시하고 내용을 보고 타입을 추측하는 동작을 끕니다. 텍스트 파일이 스크립트로 둔갑해 실행되는 공격을 막습니다. 값은 nosniff 하나뿐입니다.
  • Referrer-Policy (참조자 정보 누출 방지): 사용자가 외부 링크로 이동할 때 어디서 왔는지(전체 URL)가 상대 사이트로 새는 것을 제어합니다. 권장 값은strict-origin-when-cross-origin으로, 같은 출처에는 전체 경로를, 외부에는 도메인만 보냅니다.
  • Permissions-Policy (브라우저 기능 제한): 카메라·마이크·위치·결제 등 강력한 브라우저 기능을 페이지(및 삽입된 iframe)가 쓰지 못하게 잠급니다. 쓰지 않는 기능은 모두 꺼 두는 것이 안전합니다. 예: Permissions-Policy: geolocation=(), camera=(), microphone=().

헤더 한눈에 보기: 역할과 권장 값

헤더막는 것권장 값
Content-Security-PolicyXSS, 데이터 주입, 무단 iframedefault-src 'self'; object-src 'none'; frame-ancestors 'self' (사이트별 조정)
Strict-Transport-Security프로토콜 다운그레이드, 평문 첫 요청max-age=63072000; includeSubDomains; preload
X-Frame-Options클릭재킹(iframe 삽입)DENY 또는 SAMEORIGIN
X-Content-Type-OptionsMIME 스니핑nosniff
Referrer-Policy참조자 URL 누출strict-origin-when-cross-origin
Permissions-Policy불필요한 브라우저 기능 접근geolocation=(), camera=(), microphone=()

확인 방법과 흔한 실수

설정했다고 끝이 아닙니다. 실제로 응답에 헤더가 실려 나가는지, 오타나 잘못된 값은 없는지 검증해야 합니다. 가장 빠른 길은 보안 헤더 검사로 도메인을 넣고 어떤 헤더가 있고 빠졌는지 한눈에 확인하는 것입니다. 브라우저 개발자 도구의 Network 탭에서 응답 헤더를 직접 보거나, 터미널에서 curl -I https://example.com으로 확인할 수도 있습니다.

현장에서 가장 자주 보이는 실수는 다음과 같습니다.

  • 너무 넓은 CSP: script-src 'unsafe-inline'이나'unsafe-eval', 와일드카드 *를 넣으면 헤더는 있어도 XSS 방어 효과가 사실상 사라집니다. CSP가 깨지는 게 무서워 인라인을 통째로 허용하기보다, nonce나 해시 방식으로 필요한 인라인만 허용하세요.
  • HSTS에 무턱대고 includeSubDomains·preload: HTTPS가 안 되는 하위 도메인이 한 곳이라도 있으면 접속 불능이 됩니다. preload는 해제가 매우 느리니 전 도메인 HTTPS를 확인한 뒤에만 켜세요.
  • HTTP 응답에만 설정하고 HTTPS에는 누락: 리버스 프록시·로드밸런서 설정이 분리돼 있어 한쪽에만 헤더가 붙는 경우가 흔합니다. 양쪽 모두 확인하세요.
  • 오타·잘못된 지시어: X-Frame-Option(s 누락)처럼 철자가 틀리면 브라우저가 그냥 무시합니다. 적용한 뒤 반드시 검사 도구로 실제 반영을 확인하세요.

자주 묻는 질문

보안 헤더는 어디에 설정하나요?
애플리케이션 코드(미들웨어), 웹서버(nginx/Apache), 또는 CDN/리버스 프록시 어디서든 응답 헤더로 추가할 수 있습니다. 한 곳에서 일관되게 관리하는 것이 좋고, 여러 계층에서 중복 설정하면 값이 충돌하지 않도록 주의해야 합니다. 설정 후에는 실제 응답에 반영됐는지 반드시 검사하세요.
CSP를 켰더니 사이트가 깨졌어요.
정책이 너무 좁아 정상 스크립트·스타일·폰트의 출처가 막힌 경우입니다. 먼저 Content-Security-Policy-Report-Only 모드로 적용해 차단 없이 위반 항목만 콘솔로 받아 확인하고, 필요한 출처를 정책에 추가한 뒤 정식 모드로 전환하세요. CSP 생성기로 사이트 구조에 맞는 초안을 만들면 빠릅니다.
X-Frame-Options와 CSP frame-ancestors 중 무엇을 써야 하나요?
frame-ancestors가 더 최신이고 표현력이 좋은 표준이며, 두 헤더가 모두 있으면 브라우저는 frame-ancestors를 우선합니다. 다만 아주 오래된 브라우저는 frame-ancestors를 모르므로, 호환성을 위해 X-Frame-Options: DENY/SAMEORIGIN을 함께 두는 것이 일반적입니다.
unsafe-inline을 꼭 빼야 하나요?
가능하면 빼는 것이 맞습니다. unsafe-inline을 허용하면 인라인 스크립트가 전부 실행되므로 XSS 방어 효과가 거의 사라집니다. 인라인이 꼭 필요하면 nonce(요청마다 바뀌는 임의 토큰)나 콘텐츠 해시로 허용 범위를 그 스크립트로만 좁히세요.
HSTS preload는 켜는 게 무조건 좋은가요?
보안상으로는 강력하지만 신중해야 합니다. preload 목록에 등록되면 모든 하위 도메인까지 HTTPS가 강제되고, 목록에서 빼는 데 수개월이 걸릴 수 있습니다. 모든 (하위)도메인이 안정적으로 HTTPS를 제공한다고 확신할 때만 등록하세요.

이 가이드와 함께 쓰면 좋은 도구