보안 헤더와 CSP, 브라우저에게 미리 걸어 두는 제약
보안 헤더 검사에서 낮은 등급을 받으면 낯선 헤더 이름이 줄줄이 나온다.
각 헤더가 브라우저에게 무엇을 지시하는지, CSP는 왜 보고만 하는 모드로 먼저 켜는지 기초부터 설명한다.
이 글의 목차응답 헤더란 무엇인가7
사이트를 처음 배포하고 Mozilla의 HTTP Observatory로 검사하면 낮은 등급과 함께 Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options처럼 처음 보는 이름이 줄줄이 나온다.
모두 서버가 응답에 붙이는 보안 헤더다.
등급을 올리려고 전부 한꺼번에 켜고 싶어지지만, 잘못 걸면 멀쩡하던 기능이 멈추기도 한다.
Observatory의 채점 기준 문서도 점수와 등급은 아직 쓰지 않는 보안 기능을 알려 주려는 것이고, 어떤 것이 필요한지는 사이트마다 개발자가 판단해야 한다고 적는다.
그 판단을 하려면 헤더마다 무엇을 막는지 알아야 한다.
응답 헤더란 무엇인가
브라우저가 페이지를 요청하면 서버는 HTML 같은 본문과 함께, 그 응답을 설명하는 짧은 줄 여러 개를 보낸다.
이것을 응답 헤더라고 한다.
본문이 어떤 종류의 파일인지 알려 주는 Content-Type이 대표적이다.
보안 헤더는 그중에서 브라우저에게 지켜 달라고 요구하는 규칙이다.
페이지가 방문자의 브라우저에 도착하고 나면 서버는 더 손댈 수 없다.
그래서 규칙을 응답에 실어 보내 브라우저가 대신 지키게 한다.
파일 종류를 추측하지 말라는 헤더
Content-Type에는 text/html, text/css 같은 값이 들어간다.
이 값을 MIME 타입이라고 부른다.
실제 내용과 맞지 않는 Content-Type을 보내는 서버가 많아서, 브라우저는 내용을 들여다보고 종류를 추측해 왔다.
이 추측을 MIME 스니핑이라고 한다.
편의를 위한 동작이지만, 사용자가 올린 파일이 HTML이나 스크립트로 해석되는 길이 되기도 한다.
X-Content-Type-Options 헤더에 nosniff를 적으면 이 추측이 꺼진다.
스크립트와 스타일시트는 타입이 맞지 않으면 아예 막히고, 나머지는 서버가 알려 준 타입대로만 다뤄진다.
자바스크립트 파일이 엉뚱한 타입으로 나가고 있었다면 nosniff를 켜는 순간 그 스크립트가 막힌다.
그래서 켜기 전에 모든 파일에 Content-Type이 제대로 붙는지부터 확인한다.
다른 사이트가 내 페이지를 끼워 넣지 못하게
웹페이지는 iframe이라는 요소로 다른 사이트의 페이지를 자기 화면 안에 끼워 넣을 수 있다.
클릭재킹은 이것을 악용해 내 사이트를 보이지 않게 끼워 넣고, 그 자리에 미끼 버튼을 맞춰 두어 사용자가 의도하지 않은 동작을 누르게 하는 공격이다.
막는 방법은 X-Frame-Options 헤더나 CSP의 frame-ancestors 지시어다.
브라우저는 이 설정이 허락하지 않은 사이트의 iframe 안에서는 내 페이지를 불러오지 않는다.
끼워 넣을 필요가 없는 사이트라면 frame-ancestors를 ‘none’으로 둔다.
이 지시어는 X-Frame-Options보다 유연해서, 끼워 넣어도 되는 사이트를 골라 적을 수도 있다.
https로 돌려보내기만으로는 남는 틈
브라우저가 http 주소로 요청을 보내면, 서버는 https 주소로 다시 오라고 돌려보낸다.
그런데 돌려보내기 전의 첫 요청과 그 응답은 암호화되지 않은 채 네트워크를 지나간다.
같은 네트워크에 있는 공격자가 이 순간을 가로채면 돌려보내는 응답만으로는 막을 수 없다.
이 틈은 브라우저의 기억으로 메운다.
서버가 Strict-Transport-Security 헤더로 이 도메인은 https로만 접속하라고 알리면, 브라우저는 그 사실을 정해진 기간 동안 기억한다.
이 방식을 HSTS라고 부른다.
기억하는 동안 브라우저는 http로 가려는 시도를 스스로 https로 바꾼다.
인증서 오류가 나도 사용자가 경고를 무시하고 넘어갈 수 없게 된다.
그래서 https를 계속 제공할 준비가 안 된 채로 길게 걸면, 인증서에 문제가 생긴 날 방문자가 아예 들어오지 못한다.
처음 방문할 때는 사정이 다르다.
아직 헤더를 받은 적이 없으니 첫 요청의 틈은 그대로 남는다.
브라우저에 HSTS 도메인 목록을 미리 넣어 두는 프리로드가 이 틈을 막으려고 생겼다.
다만 요즘 Chrome과 Safari는 HSTS와 상관없이 http 주소로 가는 이동을 먼저 https로 바꿔 본다.
그래서 프리로드 목록을 운영하는 쪽도 HSTS는 권하지만, 한 번 오르면 빼는 데 몇 달이 걸리는 프리로드는 권하지 않는다.
헤더의 유지 기간인 max-age는 짧게 시작해 천천히 늘려 간다.
CSP: 실행해도 되는 출처를 미리 적어 두기
보안 헤더 가운데 손이 가장 많이 가는 것이 CSP다.
이 헤더가 막으려는 공격부터 정리한다.
XSS(크로스 사이트 스크립팅)는 공격자가 남의 사이트에 자기 스크립트를 심어 실행시키는 공격이다.
댓글이나 주소 파라미터처럼 사용자가 넣은 값을 걸러 내지 않고 페이지에 넣으면, 그 안의 스크립트가 사이트의 코드와 같은 권한으로 실행된다.
CSP(Content Security Policy)는 이 페이지가 스크립트와 스타일, 이미지를 어디서 불러와도 되는지 서버가 미리 적어 두는 헤더다.
브라우저는 목록에 없는 출처의 자원을 막는다.
허용할 것만 적고 나머지는 막는 방식은 포트와 방화벽 글에서 본 기본 거부와 같은 생각이다.
목록에서 하나를 빠뜨리면 그 자원은 막힌 채로 남는다.
스크립트 출처를 정하면 HTML 안에 직접 쓴 인라인 스크립트도 따로 허용하지 않는 한 막힌다.
꼭 필요하면 응답마다 새로 만드는 임의의 값(nonce)을 정책과 script 태그에 함께 적거나, 스크립트의 해시를 정책에 적어 허용한다.
MDN의 CSP 안내서는 nonce가 응답마다 달라야 해서 미리 만든 HTML을 그대로 내보내는 사이트에는 쓸 수 없다고 짚는다.
그런 정적인 페이지에는 내용이 같으면 값도 같은 해시를 쓴다.
CSP는 겹으로 두는 방어다.
켜 두어도 사용자가 넣은 값을 걸러 내는 일은 따로 해야 한다.
막기 전에 보고만 받아 보는 이유
실제 페이지는 생각보다 많은 곳에서 자원을 가져온다.
분석 스크립트, 웹 글꼴, 지도, 결제 창이 저마다 다른 주소를 쓴다.
정책에서 하나를 빠뜨리면 그 기능은 조용히 멈추고, 브라우저 콘솔에 정책 위반으로 막았다는 오류만 남는다.
서버 기록을 봐도 이상한 점이 없다.
서버는 HTML을 정상으로 보냈고, 자원을 막은 쪽은 방문자의 브라우저다.
그래서 CSP에는 막지 않고 위반만 알려 주는 모드가 있다.
Content-Security-Policy-Report-Only 헤더로 정책을 보내면 브라우저는 정책을 강제하지 않고, 위반이 생길 때마다 정해 둔 주소로 보고를 보낸다.
허용 목록에서 빠진 예시 지도 스크립트라도 정책 종류에 따라 실행 여부가 달라진다.
- 본문 HTML정상 응답
- 지도 스크립트위반 표시 · 허용
- 지도 기능실행됨
- 본문 HTML정상 응답
- 지도 스크립트요청 차단
- 지도 기능실행 안 됨
같은 허용 목록이라도 보고 전용은 위반을 관찰하고, 강제 정책은 허용하지 않은 자원의 실행을 막는다.
보고 전용 정책은 위반을 알리지만 이 정책으로 스크립트를 막지는 않는다.
다른 차단 정책이 없는 예시다. 실제 지도·외부 스크립트·보고 수집기를 연결하거나 이 사이트의 보안 정책을 바꾸지 않는다.
실무에서는 보고 전용으로 켜 두고 실제 방문에서 어떤 위반이 생기는지 한동안 모은다.
빠진 출처를 목록에 더하고, 새 위반이 더 나오지 않으면 그때 막는 헤더로 바꾼다.
두 헤더를 한 응답에 함께 보낼 수도 있다.
지금 정책은 막는 모드로 두고, 다음에 바꿀 정책은 보고 모드로 미리 시험하는 식이다.
어디서부터 걸면 되나
| 헤더 | 줄이는 위험 | 잘못 걸면 |
|---|---|---|
| X-Content-Type-Options | 올린 파일이 다른 종류로 해석되는 일 | 타입을 잘못 붙인 정상 스크립트도 막힌다 |
| frame-ancestors | 클릭재킹 | 일부러 끼워 넣던 곳까지 막힌다 |
| Strict-Transport-Security | 다시 찾아온 방문자의 http 접속을 가로채기 | 인증서 문제 때 접속 자체가 막힌다 |
| Content-Security-Policy | 허락하지 않은 스크립트 실행 | 필요한 자원까지 막혀 기능이 멈춘다 |
헤더는 웹 서버 설정, 호스팅 서비스의 헤더 설정, CDN처럼 응답이 마지막으로 나가는 자리에서 붙인다.
붙이는 자리를 한 곳으로 정해 두면 나중에 고칠 때 찾기 쉽다.
부담이 적은 헤더부터 켠다.
끼워 넣을 일이 없는 사이트라면 nosniff와 frame-ancestors는 Content-Type만 확인하고 바로 켜도 된다.
HSTS는 https를 계속 제공할 수 있을 때 짧은 기간으로 시작하고, CSP는 보고 전용으로 먼저 켠다.
나중에 외부 서비스를 하나 더 붙이면 그 출처를 CSP 목록에 더하는 일도 같이 챙겨야 한다.