HTTP 보안 헤더: 무엇이 중요하고 어떤 값을 넣어야 하나
각 헤더가 실제로 막아 주는 공격, 처음 설정할 값, 그리고 내 사이트를 깨뜨리지 않고 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를 거부하라고 브라우저에 지시합니다. 가장 효과적인 헤더 중 하나이면서, 이 목록에서 유일하게 실패했을 때 정말 아픈 헤더이기도 합니다. 브라우저가 그 지시를 기억하고, 이미 기억한 사용자에게 다시 손쓸 방법이 없기 때문입니다.
단계적으로 적용하세요.
max-age=300— 몇 분. 아무것도 깨지지 않는지 확인합니다.max-age=31536000— 1년. 확신이 선 뒤에 적용합니다.; includeSubDomains— 모든 서브도메인이 HTTPS를 제공하는지 확인한 뒤에만 붙입니다. 사람들이 걸려 넘어지는 지점입니다. 평문 HTTP로 남아 있던status.나mail.호스트가 한 번이라도 방문한 사람 전부에게 1년 동안 접속 불가가 됩니다.; 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-corp—SharedArrayBuffer나 정밀 타이머가 필요할 때만 쓰세요. 삽입 대상이 모두 대응하지 않는 한 광고, 삽입 영상, 대부분의 서드파티 위젯이 깨집니다.
COOP는 설정하세요. CORP는 리소스별로 판단하세요. COEP는 분명한 이유가 없다면 건드리지 마세요.
이제 보내지 않아도 되는 헤더
X-XSS-Protection— 이 헤더가 제어하던 브라우저 XSS 필터는 제거됐습니다. 지금 권장되는 값은0이거나 아예 보내지 않는 것입니다. 필터 자체가 취약점을 만들기도 했습니다.Public-Key-Pins— 폐기되어 제거됐습니다. 막은 공격보다 만들어 낸 영구 장애가 더 많았습니다.Expect-CT— 더 이상 쓰지 않습니다. 인증서 투명성은 브라우저가 알아서 강제합니다.X-Powered-By와Server의 버전 문자열 — 보안 통제는 아니지만 스택 버전을 굳이 광고할 이유도 없습니다. 지우세요.
이 헤더들이 없다고 감점하는 검사 도구는 최신 상태가 아닙니다. 더 넓게 보면 이것이 요점입니다. 헤더 검사는 점수를 올리는 용도가 아니라, 빠뜨린 것을 찾는 용도로 쓰세요.
자주 묻는 질문
어떤 보안 헤더가 실제로 중요한가요?
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 frame-ancestors가 있으면 X-Frame-Options는 필요 없나요?
frame-ancestors가 우선하며, 둘 다 있으면 frame-ancestors가 이깁니다. 다만 X-Frame-Options: DENY를 함께 보내도 비용이 들지 않고 CSP를 무시하는 클라이언트까지 덮어 주기 때문에, 대부분의 사이트는 둘 다 유지합니다.HSTS preload를 켜야 할까요?
max-age와 includeSubDomains로 한동안 운영해 보세요.내 도메인에서 직접 확인하기
위 내용은 지금 내 설정이 어떤 상태인지 볼 수 있어야 쓸모가 있습니다. 아래 도구로 바로 확인해 보세요.
이 가이드에서 잘못된 내용을 발견하셨나요? 다음 주소로 알려 주세요: [email protected] — 확인해서 고치고, 위의 수정일도 함께 갱신합니다.