보안 헤더(CSP·X-Frame-Options·X-Content-Type-Options·Referrer-Policy)는 XSS·클릭재킹·MIME 스니핑의 피해를 브라우저 단에서 줄여 주는 첫 방어선입니다. Findable은 Report-Only로 안전하게 도입한 뒤 차단 모드로 전환하고, 정적 호스팅이면 _headers 파일 하나로 무료 적용합니다. 단, 보안에 100%는 없습니다.
요약
- 네 개의 헤더가 각각 다른 공격을 막는다 — CSP는 XSS, X-Frame-Options는 클릭재킹, X-Content-Type-Options는 MIME 스니핑, Referrer-Policy는 정보 유출.
- CSP는 처음부터 차단하지 말고 Content-Security-Policy-Report-Only로 며칠 관찰한 뒤 차단 모드로 전환한다.
- inline 스크립트를 외부 파일로 빼면
script-src 'self'만으로 깔끔하게 돌아간다(unsafe-inline 불필요). - 정적 호스팅은
_headers파일 한 개로 무료 적용 — 도구가 아니라 설정의 문제다.
몇 해 전, 다른 회사가 만든 사이트를 넘겨받아 운영을 맡은 적이 있습니다. 기능은 멀쩡했습니다. 그런데 응답 헤더를 열어 보니 보안 헤더가 한 줄도 없더군요. 댓글 입력란에 누군가 <script>를 넣으면 그게 다른 방문자 브라우저에서 그대로 실행될 수 있는 상태였습니다. 코드를 크게 고친 게 아닙니다. 헤더 네 줄을 추가하고, inline 스크립트를 외부 파일로 옮겼습니다. 그날 이후로 저는 사이트를 ‘열면 끝’이 아니라 ‘열고 나서 헤더부터 본다’로 일합니다.
그래서, 이 네 줄이 뭡니까?
설명만 듣고 넘어가면 안 붙입니다. 그래서 Findable이 실제로 쓰는 헤더를 그대로 가져왔습니다. ‘복사’를 누르면 클립보드에 담깁니다. 당신 사이트의 설정 파일에 붙이고, 도메인 사정에 맞게 조금만 다듬으면 됩니다.
복사해서 바로 적용하세요
Findable이 실제로 쓰는 보안 헤더 예시입니다. '복사' 버튼으로 클립보드에 담깁니다.
X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Referrer-Policy: strict-origin-when-cross-origin Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'
이 네 줄이 각각 무슨 공격을 막는지, 저는 한 표로 묶어 두고 클라이언트에게 보여 줍니다.
이름은 길지만, 하는 일은 ‘브라우저의 친절을 한 겹씩 통제’하는 것입니다.
X-Content-Type-Options: nosniff — ‘알아서 추측’을 끈다
브라우저는 가끔 친절합니다. 서버가 “이건 텍스트야”라고 보낸 파일을 보고 “어? 이거 사실 스크립트 같은데?”라며 알아서 실행해 버리는 경우가 있습니다. 이걸 MIME 스니핑이라고 합니다. 공격자는 이 친절을 악용해, 이미지인 척 업로드한 파일을 스크립트로 실행시킵니다. nosniff 한 단어가 이 추측을 꺼서, 서버가 말한 타입 그대로만 다루게 합니다. 부작용이 거의 없어서 제가 가장 먼저 붙이는 줄입니다.
X-Frame-Options: SAMEORIGIN — 내 화면을 ‘덫’에 못 끼우게
클릭재킹은 이렇게 동작합니다. 공격자가 자기 페이지 위에 내 사이트를 보이지 않는 iframe으로 덮어씌웁니다. 사용자는 “경품 받기”를 누른다고 생각하지만, 실제로는 그 아래에 숨겨진 내 사이트의 ‘결제’ 버튼을 누르고 있는 겁니다. SAMEORIGIN은 내 페이지를 같은 도메인 외의 곳에서는 iframe으로 못 띄우게 막습니다. 최신 브라우저에서는 CSP의 frame-ancestors가 같은 일을 더 세밀하게 하지만, 구형 호환을 위해 둘을 같이 둡니다.
Referrer-Policy: strict-origin-when-cross-origin — 주소를 흘리지 않게
사용자가 내 사이트에서 외부 링크를 누르면, 기본적으로 ‘방금 어느 주소에서 왔는지’가 상대 사이트에 통째로 넘어갑니다. 그 주소에 결제 ID나 토큰 같은 게 들어 있으면 그대로 새어 나갑니다. 이 정책은 같은 사이트 안에서는 전체 주소를, 외부로 나갈 때는 도메인만 넘기고, 보안(HTTPS)에서 비보안으로 갈 때는 아예 안 넘깁니다. 사용자 추적·프라이버시 관점에서 기본값으로 두기 좋습니다.
Content-Security-Policy — XSS를 막는 핵심, 그리고 가장 까다로운 것
네 줄 중 가장 힘이 세고, 가장 신중해야 하는 줄입니다. CSP는 “이 페이지는 어디서 온 스크립트·이미지·스타일만 허용한다”를 브라우저에 못박습니다. script-src 'self'면 내 도메인에서 온 스크립트만 실행하고, 공격자가 본문에 끼워 넣은 <script>나 외부 악성 스크립트는 브라우저가 거부합니다. XSS(교차 사이트 스크립팅)의 실제 피해를 크게 줄이는 한 겹이 바로 이것입니다.
문제는, 잘못 켜면 멀쩡하던 기능이 같이 막힌다는 점입니다. 그래서 도입 방법이 중요합니다.
안 깨고 켜는 법 — Report-Only로 먼저
저는 CSP를 처음부터 차단 모드로 켜지 않습니다. 대신 Content-Security-Policy-Report-Only라는 형제 헤더를 씁니다. 이 모드는 정책을 위반해도 차단하지 않고 보고만 합니다. 며칠 띄워 두고 “어떤 스크립트·폰트·이미지가 막힐 뻔했는지”를 관찰합니다. 정당한데 막히는 게 보이면 정책에 그 출처를 추가합니다. 충분히 다듬어졌다 싶으면 그제서야 -Report-Only를 떼고 진짜 차단 헤더로 바꿉니다. 이렇게 하면 사용자에게 깨진 화면을 보여 주지 않고 안전하게 전환됩니다.
inline 스크립트를 줄이는 게 진짜 일이다
CSP가 까다로워지는 가장 큰 이유는 HTML 안에 박힌 <script>…</script>(inline) 때문입니다. 이걸 허용하려면 'unsafe-inline'을 넣어야 하는데, 그 순간 CSP의 XSS 방어 효과가 크게 떨어집니다. 이름에 ‘unsafe’가 괜히 붙은 게 아닙니다. 그래서 저는 inline 스크립트를 전부 외부 .js 파일로 빼서 script-src 'self'만으로 돌아가게 만듭니다. 위 예시에서 스타일만 'unsafe-inline'을 둔 건, 스타일은 스크립트보다 위험이 낮고 점진적으로 정리 중이기 때문입니다. 스크립트는 절대 그렇게 두지 않습니다.
CSP를 처음 켤 때 제가 실제로 쓰는 디렉티브를 ‘무엇을 허용하나’와 함께 정리하면 이렇습니다.
| 디렉티브 | 통제 대상 | 예시 값 |
|---|---|---|
| default-src | 아래에 안 적힌 모든 리소스의 기본값 | 'self' |
| script-src | 실행 가능한 스크립트 출처 | 'self' (inline 금지) |
| style-src | 스타일시트·inline 스타일 | 'self' 'unsafe-inline' |
| img-src | 이미지 출처 | 'self' data: |
| frame-ancestors | 나를 iframe에 끼울 수 있는 곳 | 'self' |
차단 모드로 바로 켜지 않고, 먼저 Report-Only로 며칠 관찰하는 헤더를 그대로 가져왔습니다. ‘복사’로 담아 며칠 띄워 두고, 막힐 뻔한 출처를 추가한 뒤 진짜 차단 헤더로 바꾸세요.
Report-Only로 먼저 켜기
차단하지 않고 위반을 보고만 합니다. 며칠 관찰 후 차단 헤더로 전환하세요.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'self'
중소기업도 무료로 — _headers 파일 한 개
“보안은 큰 회사 얘기 아니냐”는 말을 자주 듣습니다. 아닙니다. 보안 헤더는 도구를 사는 게 아니라 설정을 한 줄 쓰는 일이라 돈이 들지 않습니다. Netlify·Cloudflare Pages 같은 정적 호스팅이라면 사이트 루트에 _headers 파일을 만들고 다음처럼 적으면 끝입니다.
_headers 파일 예시
Netlify·Cloudflare Pages 루트에 두면 모든 경로에 헤더가 붙습니다. '복사'로 담아 가세요.
/* X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Referrer-Policy: strict-origin-when-cross-origin Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'
서버를 직접 운영한다면 Nginx의 add_header, Apache의 Header set에 같은 내용을 몇 줄 넣으면 됩니다. 어느 쪽이든 추가 요금은 없습니다. 적용했는지 확인은 브라우저 개발자도구의 네트워크 탭에서 응답 헤더를 보거나, securityheaders.com 같은 공개 점검 도구로 등급을 받아 보면 됩니다.
| 항목 | 헤더 없음 | 보안 헤더 적용 |
|---|---|---|
| XSS(끼워 넣은 스크립트) | 방문자 브라우저에서 실행 가능 | CSP가 외부·inline 스크립트 차단 |
| 클릭재킹(가짜 화면 덫) | iframe으로 덮어 클릭 가로채기 | X-Frame-Options로 끼우기 차단 |
| MIME 스니핑 | 이미지인 척한 파일이 실행될 수 있음 | nosniff로 추측 실행 차단 |
| 방문자 신뢰 | 경고·피해 시 신뢰 손상 | 기본 방어선으로 위험 표면 축소 |
헤더 네 줄을 진지하게 보는 사람이, 운영 전체를 본다
보안 헤더는 화면에 보이지 않습니다. 그래서 빼먹기 쉽고, 빼먹어도 한동안 아무 일도 안 일어납니다. 문제는 ‘무슨 일이 생긴 그날’에야 드러난다는 겁니다. 안 보이는 곳을 챙기는 태도는 헤더 하나에서 끝나지 않습니다. 의존성 업데이트, 백업, 접근 권한, 로그까지 같은 손으로 챙겨야 합니다. Findable에서는 이런 안 보이는 기본이 출고 체크리스트에 들어가 있습니다.
다른 담당자와의 연결
웹 보안, 무엇부터 챙길까
헤더 다음에 봐야 할 보안의 기본기.
운영 담당자의 출고 체크리스트
안 보이는 곳까지 챙기는 운영 노하우.
개발은 이렇게 합니다
inline 줄이기·웹표준·구조화 데이터 내장.
CSP를 켜면 사이트가 깨지지 않나요?
중소기업도 보안 헤더를 무료로 적용할 수 있나요?
X-Frame-Options와 CSP frame-ancestors 중 뭘 써야 하나요?
inline 스크립트가 많으면 CSP를 못 쓰나요?
보안 헤더만 붙이면 해킹을 100% 막나요?
웹 보안, 무엇부터 챙길까
보안의 기본기 정리.
운영 담당자의 출고 체크리스트
안 보이는 곳까지 챙기는 법.
구조화 데이터, 왜 넣나
검색·AI가 읽는 데이터 설계.
보안에 100%는 없습니다. 이 글은 법률·보안 자문이 아니라 운영 실무 노하우 공유입니다. 본문의 ‘복사’ 위젯은 이 페이지에서 실제로 동작하며, 제시한 헤더는 Findable이 실제로 사용하는 예시입니다. 도메인·연동 사정에 따라 정책은 다듬어야 합니다. 날조된 사례·수치는 사용하지 않았습니다.