HTTP 보안 헤더: 무엇이 중요하고 어떤 값을 넣어야 하나

수정일 2026-08-30 10분 분량

각 헤더가 실제로 막아 주는 공격, 처음 설정할 값, 그리고 내 사이트를 깨뜨리지 않고 CSP를 적용하는 순서를 정리했습니다.

헤더가 할 수 있는 일과 할 수 없는 일

보안 헤더는 내 사이트를 어떻게 다룰지 브라우저에게 주는 지시입니다. 이 표현이 중요한 이유는 한계를 함께 규정하기 때문입니다. 헤더는 취약점의 피해를 줄여 줄 뿐 취약점 자체를 없애지 않습니다. CSP는 주입된 스크립트의 실행을 막을 수는 있어도 주입 자체를 막지는 못합니다. 헤더는 첫 번째 방어선이 아니라 두 번째 방어선입니다.

대신 비용이 유난히 쌉니다. 대부분 설정 한 줄이고, 사이트 전체에 적용되며, 그 뒤로는 바꿀 일이 거의 없습니다. 비용은 낮고 효과는 실재한다는 조합, 그래서 대충이 아니라 제대로 해 둘 가치가 있습니다.

꼭 챙길 헤더와 시작 값

거의 모든 사이트에 설정할 만한 헤더
헤더시작 값막아 주는 것
Strict-Transport-Security max-age=31536000 HTTP로의 다운그레이드, 첫 요청에서의 쿠키 탈취
Content-Security-Policy default-src 'self' 주입된 스크립트, 다른 사이트의 프레임 삽입
X-Content-Type-Options nosniff 업로드된 파일이 스크립트로 재해석되는 일
Referrer-Policy strict-origin-when-cross-origin 쿼리 문자열을 포함한 전체 URL이 제3자에게 새는 일
Permissions-Policy geolocation=(), camera=(), microphone=() 삽입된 콘텐츠가 기기 API에 손대는 일
X-Frame-Options DENY CSP를 무시하는 클라이언트에서의 클릭재킹

이 중 둘은 표 한 줄로 끝낼 수 없습니다.

HSTS: 되돌리기 어려운 단 하나

Strict-Transport-Security는 지정한 초 동안 이 도메인에 대해 평문 HTTP를 거부하라고 브라우저에 지시합니다. 가장 효과적인 헤더 중 하나이면서, 이 목록에서 유일하게 실패했을 때 정말 아픈 헤더이기도 합니다. 브라우저가 그 지시를 기억하고, 이미 기억한 사용자에게 다시 손쓸 방법이 없기 때문입니다.

단계적으로 적용하세요.

  1. max-age=300 — 몇 분. 아무것도 깨지지 않는지 확인합니다.
  2. max-age=31536000 — 1년. 확신이 선 뒤에 적용합니다.
  3. ; includeSubDomains모든 서브도메인이 HTTPS를 제공하는지 확인한 뒤에만 붙입니다. 사람들이 걸려 넘어지는 지점입니다. 평문 HTTP로 남아 있던 status.mail. 호스트가 한 번이라도 방문한 사람 전부에게 1년 동안 접속 불가가 됩니다.
  4. ; preload — 사실상 영구적인 약속이라는 점을 받아들일 준비가 됐을 때만 붙입니다.

핵심은 순서입니다. 각 단계는 앞 단계가 무언가를 증명했기 때문에 안전합니다. 처음부터 전체 문자열을 넣는 것이 서브도메인이 사라지는 경로입니다.

CSP: 사이트를 깨뜨리지 않고 적용하기

Content-Security-Policy는 가장 가치 있는 헤더이자 가장 쉽게 문제를 일으키는 헤더입니다. 페이지가 정확히 무엇을 불러오는지 알아야 하기 때문입니다. 방법은 간단합니다. 브라우저가 직접 알려 주게 하면 됩니다.

report-only로 시작하기

Content-Security-Policy 대신 Content-Security-Policy-Report-Only를 보냅니다. 브라우저는 적용할 때와 똑같이 정책을 평가하고 모든 위반을 콘솔에 기록하지만 아무것도 막지 않습니다. 엄격하게 시작하세요.

Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

진짜 필요한 곳만 넓히기

홈페이지만이 아니라 모든 템플릿을 실제로 둘러보면서 위반 내역을 읽으세요. 각 항목은 정말로 불러오는 것이거나(정책에 추가), 불러오는 줄 몰랐던 것(한 번 더 살펴볼 가치가 있는 것)입니다. 분석 스크립트, 광고 스크립트, 외부 폰트, 서드파티 위젯이 여기서 드러납니다.

인라인 스크립트는 의식적으로 처리하기

인라인 <script> 블록 때문에 결국 'unsafe-inline'을 넣고 마는 경우가 많은데, 그러면 이점의 대부분이 사라집니다. 대안은 두 가지입니다.

  • 해시. 각 인라인 블록의 SHA-256을 계산해 script-src에 나열합니다. 빌드 시점에 목록이 정해지고 자동 생성할 수 있는 정적 사이트에 적합합니다.
  • Nonce. 응답마다 새 난수를 발급해 허용할 스크립트 태그와 정책 양쪽에 넣습니다. 요청마다 인라인 내용이 달라질 때 필요합니다.

어느 쪽이든 목표는 'unsafe-inline''unsafe-eval'이 없는 script-src입니다.

그다음에 적용하기

사이트 전체에서 report-only가 한동안 조용했다면 헤더 이름을 바꿉니다. 더 조이는 실험을 위험 없이 해 보고 싶다면 report-only 버전을 함께 남겨 두세요. 두 헤더는 동시에 보낼 수 있습니다.

style-src 'unsafe-inline'은 스크립트 쪽보다 훨씬 작은 양보이고, 실용적으로 받아들일 만합니다. 대부분의 프레임워크가 인라인 style 속성을 내보내는 데다 공격 가치도 훨씬 낮습니다.

Permissions-Policy

Permissions-Policy(예전 이름 Feature-Policy)는 내 페이지와 그 안에 삽입된 콘텐츠에서 브라우저 기능을 끕니다. 허용 목록이 비어 있는 ()는 "나를 포함해 아무도"라는 뜻입니다.

Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()

쓰지 않는 기능을 나열해 두면 비용 없이 삽입된 콘텐츠의 통로를 하나 줄일 수 있습니다. 내 페이지에서 필요한 기능이 있다면 self를 쓰세요. 예: fullscreen=(self)

교차 출처 격리 3종 세트

검사 도구들이 COOP, COEP, CORP를 한 묶음으로 권하는 경우가 많은데 이는 오해를 부릅니다. 이들은 더 좁은 문제(부채널을 통한 교차 출처 데이터 유출)를 다루고, 특히 COEP는 대응 헤더를 보내지 않는 서드파티 삽입 콘텐츠를 깨뜨립니다.

  • Cross-Origin-Opener-Policy: same-origin — 거의 모든 사이트에서 안전하고 설정할 가치가 있습니다. 교차 출처로 연 창과의 관계를 끊어 줍니다.
  • Cross-Origin-Resource-Policy: same-origin — 다른 사이트가 직접 링크해 쓰지 못하게 하고 싶은 리소스에 좋습니다. 삽입되기를 의도한 자산에는 무턱대고 적용하지 마세요.
  • Cross-Origin-Embedder-Policy: require-corpSharedArrayBuffer나 정밀 타이머가 필요할 때만 쓰세요. 삽입 대상이 모두 대응하지 않는 한 광고, 삽입 영상, 대부분의 서드파티 위젯이 깨집니다.

COOP는 설정하세요. CORP는 리소스별로 판단하세요. COEP는 분명한 이유가 없다면 건드리지 마세요.

이제 보내지 않아도 되는 헤더

  • X-XSS-Protection — 이 헤더가 제어하던 브라우저 XSS 필터는 제거됐습니다. 지금 권장되는 값은 0이거나 아예 보내지 않는 것입니다. 필터 자체가 취약점을 만들기도 했습니다.
  • Public-Key-Pins — 폐기되어 제거됐습니다. 막은 공격보다 만들어 낸 영구 장애가 더 많았습니다.
  • Expect-CT — 더 이상 쓰지 않습니다. 인증서 투명성은 브라우저가 알아서 강제합니다.
  • X-Powered-ByServer의 버전 문자열 — 보안 통제는 아니지만 스택 버전을 굳이 광고할 이유도 없습니다. 지우세요.

이 헤더들이 없다고 감점하는 검사 도구는 최신 상태가 아닙니다. 더 넓게 보면 이것이 요점입니다. 헤더 검사는 점수를 올리는 용도가 아니라, 빠뜨린 것을 찾는 용도로 쓰세요.

자주 묻는 질문

어떤 보안 헤더가 실제로 중요한가요?
일반적인 사이트 기준으로 가치 순서를 매기면 Content-Security-Policy(최소한 frame-ancestors라도), Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, Permissions-Policy입니다. 나머지는 상황에 따라 다릅니다. 목표는 헤더 검사 도구의 높은 점수가 아니라, 사이트가 실제로 하는 일과 맞는 정책입니다.
Content-Security-Policy를 넣으면 사이트가 깨지나요?
페이지가 무엇을 불러오는지 모르는 상태에서 바로 적용하면 깨집니다. 그래서 Content-Security-Policy-Report-Only가 있습니다. 브라우저가 정책을 그대로 평가해 무엇이 차단됐을지 보고하되 실제로는 아무것도 막지 않습니다. 위반이 사라질 때까지 report-only로 돌린 다음 적용 모드로 바꾸세요.
script-src에 unsafe-inline을 써도 되나요?
CSP가 주입 스크립트에 대해 제공하는 보호의 대부분을 없애기 때문에 임시 상태로만 두세요. 벗어나는 방법은 두 가지, 응답마다 발급하는 nonce와 인라인 블록마다의 SHA-256 해시입니다. 해시는 빌드 시점에 인라인 스크립트가 정해지는 정적 사이트에, nonce는 요청마다 내용이 달라지는 동적 페이지에 맞습니다.
CSP frame-ancestors가 있으면 X-Frame-Options는 필요 없나요?
현재 모든 브라우저에서 frame-ancestors가 우선하며, 둘 다 있으면 frame-ancestors가 이깁니다. 다만 X-Frame-Options: DENY를 함께 보내도 비용이 들지 않고 CSP를 무시하는 클라이언트까지 덮어 주기 때문에, 대부분의 사이트는 둘 다 유지합니다.
HSTS preload를 켜야 할까요?
현재는 물론 앞으로도 모든 서브도메인이 HTTPS를 계속 제공한다고 확신할 때만 켜세요. preload는 도메인을 브라우저 바이너리에 새기는 것이라, 해제 요청을 넣어도 사용자에게 반영되기까지 몇 달이 걸립니다. 먼저 긴 max-ageincludeSubDomains로 한동안 운영해 보세요.

내 도메인에서 직접 확인하기

위 내용은 지금 내 설정이 어떤 상태인지 볼 수 있어야 쓸모가 있습니다. 아래 도구로 바로 확인해 보세요.

이 가이드에서 잘못된 내용을 발견하셨나요? 다음 주소로 알려 주세요: [email protected] — 확인해서 고치고, 위의 수정일도 함께 갱신합니다.