요약

  • W3C가 2011년 11월 29일 공개한 첫 콘텐츠 보안 정책 초안은 사이트가 자원 출처를 선언하고 브라우저가 실제 로드 요청을 검사하는 방식을 제안했다.
  • 2016년 12월 15일의 Level 2 권고안은 강제 정책과 보고 전용 정책, 선택한 인라인 스크립트를 허용하는 nonce와 해시를 설명했다. 명단은 사이트가 쓰고 문은 브라우저가 지킨다.

웹사이트는 잔치를 벌이고 초대장 장부를 잃어버렸다. 익숙한 공급자의 스크립트가 오고, 설치한 이유를 아무도 기억하지 못하는 옛 위젯의 스크립트도 왔다. 이미지와 글꼴이 줄을 섰고, 먼 주소로 향하는 연결이 뒤따랐다. 낯선 코드 하나가 오래된 친구처럼 행동하며 틈에 끼어들었다. 문지기를 세우기는 쉬웠다. 그에게 제대로 된 손님 명단을 건네는 일이 어려웠다.

W3C가 2011년 11월 29일 낸 콘텐츠 보안 정책의 첫 공개 작업 초안은 역할을 나눴다. 초안은 웹 애플리케이션이 자원을 불러올 출처를 제한하는 선언적 정책을 제시했다. 목적은 사이트 간 스크립팅을 포함한 콘텐츠 주입 위험을 줄이는 것이었다. 사이트가 허용할 출처를 말하면, 브라우저는 실제 자원 요청이 생길 때 그 규칙에 맞춰 판단했다.

명단은 모든 손님에게 붙이는 막연한 ‘신뢰’ 딱지가 아니었다. 초안은 기본 규칙인 default-src 외에 script-src, img-src, style-src, font-src, connect-src 등 자원 종류별 지시문을 나눴다. 사진은 여러 곳에서 받아도, 실행할 코드는 더 좁은 곳에서 받게 할 수 있었다. 벽에 걸릴 그림과 무대 조명실을 움직일 사람에게 같은 열쇠를 줄 이유는 없다. CSP는 일에 따라 문을 따로 만들었다.

새 문은 사이트의 사정도 드러냈다. 제작자는 쓰려는 공급자를 알았고, 브라우저는 페이지가 실제로 요구한 주소를 봤다. 정책은 두 목록을 맞댔다. script-src 한 줄을 쓰는 데는 오래 걸리지 않는다. 몇 년 전 행사 페이지가 아직도 이름 없는 부품에 기대고 있는지 찾는 데는 시간이 든다. 명단에서 합법적인 결제 구성요소를 빠뜨리면 문지기는 수상한 코드와 마찬가지로 결제를 막는다. 그때 드러나는 것은 브라우저의 고집이 아니라 주인의 허술한 장부다.

2016년 12월 15일 발표된 W3C 콘텐츠 보안 정책 Level 2 권고안은 본격적인 차단에 앞서 리허설을 마련했다. Content-Security-Policy-Report-Only 응답 헤더를 쓰면 브라우저가 제안된 규칙을 어긴 요청을 관찰하고 보고하면서 자원은 계속 불러온다. 문서는 이 방식으로 정책을 다듬은 뒤 강제력을 가진 Content-Security-Policy 헤더로 옮겨 가는 예를 제시한다. 문지기는 먼저 수첩을 들고 줄을 살핀 다음에야 입구에 밧줄을 친다.

보고서는 사이트가 잊고 있던 의존성을 드러낼 수 있다. 예상 밖 주소에서 온 글꼴, 오래된 페이지의 분석 도구, 담당자가 없는 구성요소가 만든 연결 같은 것들이다. 브라우저는 자신이 본 요청을 적는다. 운영팀은 그 자원이 원하는 기능인지, 페이지에서 뺄지, 규칙을 고칠지 결정한다. 보고 전용 모드는 회의실 칠판에 그린 단정한 사이트와 실제로 돌아가는 사이트 사이의 차이를 눈앞에 펼친다.

정책을 어디에 놓는지도 중요했다. 2016년 권고안은 페이지 처리 초기에 브라우저가 받는 HTTP 응답 헤더를 우선했다. 문서 앞부분의 HTML meta 요소에도 일부 정책을 둘 수 있었다. 다만 그 요소보다 먼저 불러온 자원에는 적용되지 않고, 보고 전용 정책을 전달할 수도 없었다. 관객 절반이 들어온 다음 로비에 입장 규칙을 붙여 봐야 이미 지난 사람은 검사할 수 없다. 헤더는 문을 여는 순간 도착한다.

페이지 안에 직접 쓴 스크립트는 가장 까다로운 손님이었다. 사이트에 필요한 코드도 있지만, 인라인 코드 전체를 허용하면 주입된 코드도 함께 들어온다. Level 2는 nonce와 해시로 특정 블록을 가리키는 방법을 설명했다. 서버가 응답마다 만든 무작위 값을 정책과 허용할 스크립트 요소에 함께 넣는다. 해시는 스크립트의 정확한 내용과 맞는다. ‘JavaScript’라는 표를 든 누구나 통과시키는 대신, 지목한 손님에게만 초대장을 주는 셈이다.

문은 하나가 아니었다. img-src는 이미지, connect-src는 특정 연결, base-uri는 상대 링크의 기준 주소, form-action은 양식 제출 목적지, frame-ancestors는 사이트를 프레임 안에 보여 줄 수 있는 페이지를 다룬다. 서로 다른 행동에 서로 다른 규칙이 붙는다. 사이트가 의도를 쓰고 브라우저가 그 행동이 일어나는 순간 확인한다.

W3C의 CSP Level 2 발행 기록에는 여러 초안을 거쳐 2016년 12월 권고안에 이른 과정이 남아 있다. 날짜는 규격 문서의 이정표다. 오래 남은 변화는 실제 일의 분배에 있다. 사이트가 허용할 출처를 밝히고, 브라우저가 자원 요청을 검사하고, 보고가 계획과 현실의 간격을 보여 준다. 문지기는 지침을 받았고 주인은 자기 잔치의 손님부터 알아야 했다.

독자는 기사를 읽거나 계산을 할 때 script-src 헤더를 먼저 펼치지 않는다. 페이지가 작동하는지, 빠진 이름 때문에 중요한 기능이 멈췄는지만 느낀다. 손님 명단은 무대 뒤의 일이다. 브라우저는 초대받지 않은 코드를 막을 수 있다. 사이트는 그 전에 자기가 누구를 불렀는지 말할 수 있어야 한다. 웹의 보안 향상이 뜻밖에도 출석 확인에서 시작한 것이다.