2026 웹 보안 실전 가이드

9장. HSTS와 보안 헤더 — 한 줄씩

HSTS preload의 함의와 위험, Content-Security-Policy 설계, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy.

보안 헤더는 브라우저에게 보내는 명령입니다. 한 줄의 헤더가, 수많은 공격의 부류 전체를 막을 수 있습니다.

들어가며: 브라우저에게 어떻게 행동할지 지시하기

8장에서 우리는 통신을 단단히 암호화했습니다. 그러나 전송 계층 방어는 암호화만으로 완성되지 않습니다. 또 하나의 강력하면서도 종종 간과되는 방어 계층이 있습니다. 바로 보안 헤더(security headers)입니다.

보안 헤더가 무엇인지부터 이해해 보겠습니다. 당신의 서버가 브라우저에 웹 페이지를 보낼 때, 페이지 내용만 보내는 것이 아닙니다. 그와 함께 일련의 지시사항 — HTTP 응답 헤더 — 을 함께 보냅니다. 이 헤더 중 일부는 브라우저에게 "이 사이트를 다룰 때 이렇게 행동하라"는 보안 관련 명령을 담을 수 있습니다. 이것이 보안 헤더입니다.

보안 헤더의 강력함은, 한 줄의 지시로 공격의 부류 전체를 막을 수 있다는 데 있습니다. 예를 들어, 한 줄의 헤더로 "이 사이트는 항상 암호화된 연결로만 접속하라"고 지시하면, 암호화되지 않은 연결을 노리는 공격 전체가 무력해집니다. 또 다른 헤더로 "이 사이트를 다른 사이트의 틀 안에 넣지 말라"고 지시하면, 사용자를 속여 클릭을 가로채는 공격을 막습니다. 적은 노력으로 큰 방어 효과를 얻는, 비용 대비 효율이 매우 높은 방어입니다.

그런데도 서문에서 지적했듯, 보안 헤더가 단 하나도 설정되지 않은 사이트가 흔합니다. 헤더를 설정하지 않으면 브라우저는 기본 동작을 하는데, 그 기본 동작은 대개 호환성을 위해 느슨한 쪽입니다. 명시적으로 안전한 동작을 지시하지 않으면, 브라우저는 안전하지 않을 수 있는 기본값으로 작동합니다.

이 장에서는 주요 보안 헤더를 하나씩, 한 줄씩 살펴봅니다. 8장에서 예고한 A+의 마지막 열쇠인 HSTS부터 시작하여, 가장 강력하지만 가장 까다로운 CSP, 그리고 적은 노력으로 즉시 효과를 보는 여러 헤더들까지 다룹니다. 각 헤더가 무엇을 막는지, 어떻게 설정하는지, 그리고 어떤 함정이 있는지를 정리합니다.

9.1 HSTS: 항상 HTTPS로만

8장에서 A+의 마지막 열쇠로 예고한 헤더가 바로 HSTS입니다. HSTS(HTTP Strict Transport Security)는 브라우저에게 "이 사이트는 앞으로 항상 HTTPS로만 접속하라"고 지시하는 헤더입니다.

HSTS가 막는 것

HTTPS를 쓰더라도, 미묘한 약점이 하나 남습니다. 사용자가 주소창에 example.com이라고만 입력하면, 브라우저는 처음에 암호화되지 않은 HTTP로 접속을 시도하고, 서버가 그제야 HTTPS로 안내하는 경우가 많습니다. 이 최초의 짧은 순간 — 암호화되기 전의 HTTP 연결 — 이 공격의 틈이 됩니다. 공격자가 이 순간을 가로채, 사용자를 가짜 사이트로 유도하거나 암호화로의 전환을 방해할 수 있습니다(이런 공격을 SSL 스트립이라 부릅니다).

HSTS는 이 틈을 막습니다. 한 번 HSTS 헤더를 받은 브라우저는, 그 이후 그 사이트에 대해서는 처음부터 HTTPS로만 접속합니다. 사용자가 HTTP를 입력하거나 HTTP 링크를 클릭해도, 브라우저가 자체적으로 HTTPS로 바꿔 접속합니다. 암호화되지 않은 연결이 아예 발생하지 않으므로, 그 틈을 노리는 공격이 성립하지 않습니다. (헤더의 정확한 동작과 옵션은 MDN의 Strict-Transport-Security 문서에 정리되어 있습니다.)

HSTS 설정과 그 의미

HSTS 헤더는 다음과 같은 형태입니다.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

각 부분의 의미를 정확히 이해해야 합니다. 이것이 이 헤더가 "까다로운" 이유이기 때문입니다.

max-age — 브라우저가 이 지시를 얼마 동안 기억할지를 초 단위로 지정합니다. 위 예시의 값은 약 1년입니다. 이 기간 동안 브라우저는 해당 사이트를 HTTPS 전용으로 취급합니다.

includeSubDomains — 이 지시를 모든 하위 도메인에도 적용합니다. 이것은 강력하지만 주의가 필요합니다. 만약 어떤 하위 도메인이 아직 HTTPS를 지원하지 않는다면, 이 옵션을 켜는 순간 그 하위 도메인에 접속할 수 없게 됩니다. 따라서 이 옵션을 켜기 전에, 5장에서 만든 서브도메인 인벤토리를 점검하여 모든 하위 도메인이 HTTPS를 지원하는지 확인해야 합니다.

preload — 이것은 가장 강력하고 가장 되돌리기 어려운 옵션입니다. 이 옵션은 당신의 사이트를 브라우저에 내장된 HSTS 사전 목록에 등록하겠다는 신청입니다. 등록되면, 브라우저는 당신의 사이트에 단 한 번도 방문한 적 없어도 처음부터 HTTPS로만 접속합니다. 가장 완벽한 보호이지만, 가장 위험하기도 합니다.

preload의 위험: 되돌리기 어렵다

preload의 위험을 특별히 강조하겠습니다. 일단 사이트가 브라우저의 사전 목록에 등록되면, 그것을 되돌리는 것은 매우 느리고 어렵습니다. 목록에서 제거를 신청해도, 이미 배포된 수많은 브라우저에서 그 정보가 사라지기까지 오랜 시간이 걸립니다.

이것이 왜 위험할까요? 만약 어떤 이유로 당신의 사이트나 하위 도메인이 HTTPS를 제공할 수 없게 된다면 — 인증서 문제, 설정 오류, 또는 HTTP만 쓰는 하위 도메인의 필요 — preload에 등록된 사이트는 그냥 접속 불가능해집니다. 브라우저가 HTTPS를 고집하는데 HTTPS가 안 되니, 사용자는 사이트에 들어갈 방법이 없습니다. 그리고 이것을 빠르게 되돌릴 수 없습니다.

그래서 권장하는 접근은 단계적입니다. 먼저 짧은 max-age로 시작하여 문제가 없음을 확인하고, 점차 기간을 늘리며, 모든 하위 도메인의 HTTPS 지원을 확실히 한 뒤에 includeSubDomains를 켜고, 모든 것이 안정적으로 작동함을 충분히 확인한 후에야 비로소 preload를 신청하는 것입니다. 서두르지 마십시오. preload는 한 번 켜면 빠르게 끌 수 없다는 것을 기억하십시오.

A+와의 관계

8장에서 예고한 대로, 올바르게 설정된 HSTS는 SSL Labs에서 A+를 받기 위한 핵심 요소입니다. 충분한 max-age를 가진 HSTS 헤더가 있으면 A+에 도달할 수 있습니다. 다만 위에서 강조했듯, A+ 등급 자체보다 중요한 것은 HSTS의 실제 보안 의미와 그 위험을 이해하고 안전하게 적용하는 것입니다. 등급을 위해 무작정 preload를 켰다가 되돌릴 수 없는 곤경에 빠지는 것은 본말이 전도된 것입니다.

9.2 Content-Security-Policy: 가장 강력하고 가장 까다로운

보안 헤더 중 가장 강력한 방어를 제공하지만, 동시에 가장 설정하기 까다로운 것이 CSP(Content-Security-Policy)입니다. 이 헤더는 별도의 깊은 논의가 필요할 만큼 풍부하지만, 여기서는 핵심 개념과 실용적 접근을 다룹니다.

CSP가 막는 것: XSS

CSP의 주된 목적은 교차 사이트 스크립팅(XSS, Cross-Site Scripting)이라는 공격을 막는 것입니다. XSS는 12장에서 더 다루지만, 간단히 말하면 공격자가 당신의 웹 페이지에 악성 스크립트를 주입하여, 그것이 다른 사용자의 브라우저에서 실행되게 만드는 공격입니다. 이를 통해 공격자는 사용자의 세션을 탈취하거나, 페이지를 변조하거나, 사용자의 정보를 빼낼 수 있습니다. XSS는 웹 애플리케이션에서 가장 흔하고 영향이 큰 취약점 부류 중 하나입니다. 2026년 현재도 마찬가지여서, OWASP Top 10에서 XSS는 (인젝션 범주로) 수년째 상위권을 떠나지 않습니다. 새로운 공격이 아니라 끈질긴 공격이라는 뜻이고, 그래서 CSP라는 브라우저 차원의 방어선이 여전히 값집니다.

CSP는 이 공격에 대한 강력한 방어선입니다. CSP의 핵심 발상은, 브라우저에게 "이 페이지에서 어떤 출처의 자원만 실행하거나 로드할 수 있는지"를 명시적으로 알려 주는 것입니다. 허용된 출처의 목록을 정해 두면, 그 목록에 없는 출처의 스크립트는 — 설령 공격자가 페이지에 주입하는 데 성공하더라도 — 브라우저가 실행을 거부합니다. 주입은 되어도 실행은 안 되는 것입니다. (지시어 하나하나의 의미는 MDN의 Content-Security-Policy 문서에 가장 정확하게 정리되어 있으니, 정책을 짤 때 곁에 두십시오.)

왜 까다로운가

CSP가 까다로운 이유는, 그 강력함의 이면입니다. CSP를 엄격하게 설정하면, 정당한 자원까지 차단되어 사이트가 제대로 작동하지 않을 수 있습니다. 현대의 웹 페이지는 다양한 출처에서 많은 자원(스크립트, 스타일, 이미지, 폰트, 외부 서비스 등)을 불러오는데, CSP는 이 모든 것을 명시적으로 허용해야 합니다. 하나라도 빠뜨리면 그 기능이 깨집니다.

그래서 CSP 설정은 신중한 작업입니다. 너무 느슨하면 보호 효과가 없고, 너무 엄격하면 사이트가 망가집니다. 그 균형을 찾는 것이 CSP의 어려움입니다.

실용적 접근: 관찰 모드부터

CSP를 안전하게 도입하는 핵심 기법이 있습니다. CSP에는 실제로 차단하지 않고 위반 사항을 보고만 하는 관찰 모드가 있습니다. 이 모드로 먼저 정책을 적용하면, 실제 사이트 동작에는 영향을 주지 않으면서, "만약 이 정책을 강제했다면 무엇이 차단되었을지"를 보고받을 수 있습니다.

이 관찰 모드를 활용하는 절차는 다음과 같습니다. 먼저 원하는 정책을 관찰 모드로 적용하고, 일정 기간 동안 위반 보고를 수집합니다. 그 보고를 분석하여, 정당한 자원인데 차단될 뻔한 것들을 정책에 추가합니다. 이 과정을 반복하여 정책을 다듬은 뒤, 더 이상 정당한 자원이 차단되지 않음을 확인하면, 비로소 관찰 모드를 강제 모드로 전환합니다. 이렇게 하면 사이트를 망가뜨리지 않으면서 안전하게 CSP를 도입할 수 있습니다.

CSP는 한 번에 완벽하게 설정하려 하지 말고, 이렇게 점진적으로 다듬어 가는 것이 현실적입니다. 그리고 사이트가 변하면 CSP도 갱신되어야 하므로, 이것 역시 원칙 5(과정으로서의 보안)의 대상입니다.

한 가지 더. 정책을 어렵게 다듬었는데도 실은 우회 가능한 경우가 의외로 많습니다. 예를 들어 흔히 쓰는 unsafe-inline이나, 인기 CDN 도메인을 통째로 허용하는 식의 정책은 보기에는 그럴듯해도 실제로는 XSS를 거의 막지 못합니다. 그래서 정책을 강제하기 전에 CSP Evaluator에 한 번 붙여 보길 권합니다. 작성한 정책을 넣으면 약한 지시어와 우회 가능 지점을 짚어 줍니다. 내가 운영하는 사이트에서도, "관찰 모드로 충분히 다듬었다"고 믿었던 정책이 이 도구에서 우회 가능 판정을 받아 다시 손본 적이 여러 번 있습니다.

9.3 즉시 효과를 보는 헤더들

CSP만큼 까다롭지 않으면서도, 설정하면 즉시 효과를 보는 보안 헤더들이 있습니다. 이들은 대개 한 줄로 설정되고 부작용이 적어, 우선적으로 적용하기 좋습니다. 하나씩 살펴보겠습니다. (각 헤더가 받을 수 있는 값과 브라우저 지원 범위는 MDN Web Docs에서 헤더 이름으로 검색하면 바로 확인할 수 있습니다.)

X-Content-Type-Options

X-Content-Type-Options: nosniff

이 헤더는 브라우저가 콘텐츠의 유형을 임의로 추측(스니핑)하지 못하게 합니다. 브라우저는 때로 서버가 알려 준 콘텐츠 유형을 무시하고 스스로 추측하는데, 이 동작이 공격에 악용될 수 있습니다. 예를 들어, 이미지로 위장한 파일이 스크립트로 해석되어 실행되는 식입니다. nosniff를 설정하면 브라우저가 서버가 명시한 유형을 그대로 따르므로, 이런 혼동을 이용한 공격을 막습니다. 부작용이 거의 없으므로 항상 켜는 것이 좋습니다.

X-Frame-Options

X-Frame-Options: DENY

이 헤더는 당신의 페이지가 다른 페이지의 틀(프레임) 안에 삽입되는 것을 막습니다. 이것이 왜 중요할까요? 클릭재킹(clickjacking)이라는 공격 때문입니다. 공격자는 당신의 사이트를 투명한 틀 안에 숨겨 놓고, 그 위에 가짜 화면을 덮어, 사용자가 가짜 화면을 클릭한다고 생각하지만 실제로는 당신의 사이트에서 의도하지 않은 동작을 하게 만듭니다. DENY는 어떤 틀 안에도 삽입을 금지하며, 필요에 따라 같은 출처에서만 허용하는 옵션(SAMEORIGIN)도 있습니다. (참고로 CSP에도 같은 기능을 하는 더 현대적인 지시어가 있어, CSP를 충실히 설정하면 이 기능을 그쪽에서 다룰 수도 있습니다.)

Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

사용자가 당신의 사이트에서 다른 사이트로 이동할 때, 브라우저는 "어디서 왔는지"에 대한 정보(리퍼러)를 그 사이트에 전달할 수 있습니다. 이 정보에 민감한 내용(전체 경로, 쿼리 등)이 담겨 외부로 새어 나갈 수 있습니다. Referrer-Policy는 이 정보를 얼마나 전달할지를 제어합니다. 위 예시의 설정은 합리적인 균형으로, 같은 출처로는 충분한 정보를 주되 다른 출처로는 최소한의 정보만 전달하여 프라이버시를 보호합니다. 이것은 1장에서 다룬 정보 노출을 줄이는 한 방법이기도 합니다.

Permissions-Policy

Permissions-Policy: geolocation=(), microphone=(), camera=()

이 헤더는 당신의 페이지가 브라우저의 강력한 기능들(위치 정보, 마이크, 카메라 등)을 사용할 수 있는지를 제어합니다. 대부분의 사이트는 이런 기능을 쓸 필요가 없으므로, 명시적으로 비활성화하면 됩니다. 만약 당신의 사이트가 침해되더라도, 이런 민감한 기능에 접근하지 못하게 미리 막아 두는 것입니다. 이것은 5장의 공격면 축소를 브라우저 기능 수준에서 적용하는 것입니다. 필요 없는 권한은 끄는 것이 원칙입니다.

9.4 헤더 설정의 실제와 함정

이제 이 헤더들을 실제로 적용하는 방법과 흔한 함정을 다룹니다.

어디에 설정하는가

보안 헤더는 보통 웹 서버(Nginx, Apache 등)의 설정에서 추가하거나, 애플리케이션 코드에서 추가하거나, 6장에서 다룬 Cloudflare 같은 프록시 계층에서 추가할 수 있습니다. 어느 곳에서 설정하든 효과는 비슷하지만, 한곳에서 일관되게 관리하는 것이 혼란을 줄입니다.

Nginx의 경우, 다음과 같은 형태로 헤더를 추가합니다.

# Nginx 설정 예시 (server 블록 내)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# CSP는 9.2의 절차에 따라 신중히 도입

여기서 always를 붙이는 것을 잊지 마십시오. 이것이 없으면 일부 응답(오류 응답 등)에는 헤더가 빠질 수 있습니다.

흔한 함정

중복 설정. 여러 곳(서버, 애플리케이션, 프록시)에서 같은 헤더를 설정하면, 충돌하거나 중복될 수 있습니다. 어디서 헤더가 설정되는지 파악하고, 한곳에서 일관되게 관리하십시오.

오류 페이지에서 헤더 누락. 위에서 언급한 always의 문제입니다. 정상 페이지에는 헤더가 있는데 오류 페이지에는 없으면, 그 틈이 약점이 됩니다.

CSP를 무작정 엄격하게. 9.2에서 강조했듯, CSP를 관찰 모드 없이 바로 엄격하게 적용하면 사이트가 깨집니다. 반드시 점진적으로 도입하십시오.

HSTS preload를 성급하게. 9.1에서 강조했듯, preload는 되돌리기 어렵습니다. 모든 준비가 확실해진 뒤에만 신청하십시오.

설정하고 검증하지 않음. 8장에서와 마찬가지로, 헤더를 설정한 뒤 실제로 적용되었는지 확인하지 않는 실수가 흔합니다. 다음 절에서 검증 방법을 다룹니다.

헤더 검증하기

설정한 헤더가 실제로 적용되었는지는 직접 확인할 수 있습니다. 자신의 사이트에 요청을 보내 응답 헤더를 살펴보는 것입니다.

# 자신의 사이트의 응답 헤더 확인
curl -sI https://example.com

# 보안 헤더만 추려서 보기
curl -sI https://example.com | grep -iE \
  "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"

이 출력에서 의도한 헤더들이 모두 보이는지, 값이 올바른지 확인하십시오. 빠진 헤더가 있다면 설정을 점검해야 합니다. 이런 헤더 점검을 체계적으로 수행하고 등급을 매기는 것이, 다음 장(10장)에서 다룰 자가 진단의 일부입니다. curl -sI로 응답 헤더를 직접 보거나 securityheaders.com·Mozilla Observatory 같은 온라인 점검으로, 어떤 헤더가 있고 없는지와 개선점을 한 번에 확인할 수 있습니다(10장).

두 도구의 쓰임은 조금 다릅니다. securityheaders.com은 주소를 넣으면 헤더 구성을 A~F 등급으로 빠르게 보여 줘, 무엇이 빠졌는지 한눈에 잡기 좋습니다. Mozilla Observatory는 헤더뿐 아니라 여러 보안 항목을 함께 점수화해 더 종합적인 진단을 줍니다. 처음에는 securityheaders.com으로 빠진 헤더를 메우고, 그다음 Observatory로 전반을 점검하는 순서가 편합니다. 다만 8장에서 SSL Labs를 두고 강조했던 것과 같습니다. 등급(A+) 자체가 목표가 아니라, 각 헤더가 실제로 무엇을 막는지 이해하고 켜는 것이 목적입니다. 점수만 보고 의미 없이 헤더를 욱여넣지는 마십시오.

9.5 이 장이 남기는 교훈

이 장에서 확인한 것을 압축합니다.

첫째, 보안 헤더는 브라우저에게 보내는 명령입니다. 한 줄의 헤더가 공격의 부류 전체를 막을 수 있는, 비용 대비 효율이 매우 높은 방어입니다. 설정하지 않으면 브라우저는 느슨한 기본값으로 작동합니다.

둘째, HSTS는 항상 HTTPS만을 강제하여 암호화 이전의 틈을 막습니다. A+의 마지막 열쇠이기도 합니다. 다만 includeSubDomains는 모든 하위 도메인의 HTTPS 지원을 확인한 뒤에, preload는 되돌리기 어려우므로 모든 준비가 확실해진 뒤에만 적용하십시오.

셋째, CSP는 가장 강력하지만 가장 까다롭습니다. XSS를 막는 핵심 방어이지만, 엄격하면 사이트가 깨질 수 있습니다. 관찰 모드로 시작하여 점진적으로 다듬은 뒤 강제하십시오.

넷째, 즉시 효과를 보는 헤더들을 우선 적용하십시오. X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy는 부작용이 적고 한 줄로 설정됩니다. 필요 없는 브라우저 권한은 끄는 것이 공격면 축소입니다.

다섯째, 설정하고 반드시 검증하십시오. always를 붙이고, 중복을 피하고, 오류 페이지에도 헤더가 적용되는지 확인하며, 실제 응답 헤더를 점검하십시오(원칙 4).

이것으로 3부 "전송 계층 방어"를 마칩니다. 우리는 통신을 단단히 암호화하고(8장), 브라우저에게 안전하게 행동하도록 지시했습니다(9장). 그런데 8장과 9장 내내 반복된 한 가지가 있습니다. "설정했다고 믿지 말고 측정하라"는 것입니다. SSL Labs로 TLS를 측정하고, 응답 헤더로 보안 헤더를 확인하라는 것이었습니다.

이 "측정하고 검증하라"는 정신이, 다음 4부의 주제입니다. 4부에서는 방어자가 공격자의 시선을 빌려 자신을 점검하는 법을 다룹니다. 그 첫 장에서, 자신의 사이트를 직접 스캔하여 약점을 찾아내는 체계적인 방법 — 그리고 그것을 도와주는 도구들 — 을 살펴보겠습니다.

이 장의 실행 항목

  1. 즉시 효과를 보는 헤더들을 먼저 적용하십시오. X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy. (9.3)
  2. HSTS를 단계적으로 적용하십시오. 짧은 max-age → 하위 도메인 확인 후 includeSubDomains → 모든 준비 후 preload. (9.1)
  3. CSP를 관찰 모드로 도입하십시오. 위반 보고를 분석해 정책을 다듬은 뒤 강제하십시오. (9.2)
  4. 모든 헤더에 always를 붙이고 중복을 피하십시오. 오류 페이지에도 적용되게 하십시오. (9.4)
  5. 응답 헤더를 직접 검증하십시오. curl -sI로 의도한 헤더가 적용되었는지 확인하십시오. (9.4)
  6. 헤더를 정기적으로 점검하십시오. curl -sIsecurityheaders.com·Mozilla Observatory로 빠진 헤더와 개선점을 파악하고, CSP는 강제 전 CSP Evaluator로 우회 가능 여부를 확인하십시오. (9.2, 9.4, 10장)

다음 장: 10장 — 자기 사이트를 직접 스캔하라. 공격자의 시선으로 자신을 점검하는 법, 그리고 그것을 돕는 도구들. 4부 "검증과 공격적 보안"의 시작.