![[실전 보안 09] 브라우저에게 규칙을 알려주는 CSP](https://img.thenullpage.com/posts/5716/5716_1_2bb8b5.webp)
지난 편 끝에서 입력 정화가 첫 번째 벽이라면, 그 벽이 뚫렸을 때를 대비한 두 번째 벽이 콘텐츠 보안 정책이라고 했다. 이번 편의 주제가 바로 그 정책이다. 세 글자 약자로 부르는 이 방어는, 브라우저에게 이 사이트에서는 어떤 자원만 불러오고 실행하라고 미리 규칙을 알려 주는 방식이다. 개념은 강력하지만 실제로 적용해 보니 이상과 현실 사이에서 꽤 많은 타협을 해야 했다. 우리 커뮤니티에 이 정책을 붙이며 겪은 고민과 절충을 솔직하게 정리해 본다.
1. 정책이 브라우저에게 하는 말
콘텐츠 보안 정책의 작동 원리는 의외로 직관적이다. 서버가 페이지를 내려보낼 때 함께 딸려 보내는 규칙 목록이 있다. 이 규칙은 브라우저에게 이렇게 말한다. 스크립트는 이런 출처에서 온 것만 실행해라, 이미지는 이런 곳에서만 불러와라, 스타일은 여기서만 받아라. 브라우저는 이 규칙을 읽고, 규칙에 어긋나는 자원의 로딩이나 실행을 스스로 거부한다.
이게 왜 강력한 방어가 되는지는 앞 편의 공격을 떠올리면 분명해진다. 악성 스크립트가 입력 정화를 뚫고 페이지에 들어왔다고 하자. 그 스크립트는 대개 훔친 정보를 공격자의 서버로 보내려 하거나, 외부에서 추가 악성 코드를 불러오려 한다. 그런데 콘텐츠 보안 정책이 우리가 허락한 출처 외의 통신과 실행을 막고 있으면, 그 스크립트는 페이지에 들어왔어도 실제로 아무것도 하지 못한다.
특히 페이지 안에 직접 박힌 스크립트, 즉 인라인 스크립트의 실행을 막는 게 이 정책의 핵심 무기다. 앞 편의 공격은 대개 사용자 콘텐츠 자리에 스크립트를 직접 심는 형태다. 정책으로 인라인 스크립트 실행을 금지하면, 공격자가 아무리 스크립트를 페이지에 끼워 넣어도 브라우저가 그걸 실행하지 않는다. 공격의 가장 흔한 통로 하나를 원천적으로 막는 셈이다.
이 방어가 매력적인 건 우리 코드의 실수를 브라우저가 덮어 준다는 점이다. 우리가 정화를 완벽하게 못 했더라도, 브라우저 차원에서 한 번 더 걸러 준다. 그래서 이 정책은 첫 방어가 아니라 보조 방어, 즉 다른 방어가 실패했을 때 작동하는 안전망으로 이해해야 했다. 이것만 믿고 정화를 소홀히 하면 안 되고, 반대로 정화를 했다고 이걸 안 붙일 이유도 없었다.
2. 이상적인 정책과 현실의 벽
자료를 찾아보니 권장되는 방향은 분명했다. 인라인 스크립트를 전면 금지하고, 꼭 필요한 스크립트에는 매 요청마다 바뀌는 일회용 표식을 붙여 그것만 실행을 허용하는 방식이 가장 안전하다고 했다. 이렇게 하면 공격자가 심은 스크립트에는 그 표식이 없으니 절대 실행되지 못한다. 엄격한 정책일수록 우회가 어렵다는 것이 업계의 결론이었다.
하지만 우리 사이트의 현실은 그리 깔끔하지 않았다. 오래 쌓여 온 코드 곳곳에 인라인 스크립트와 인라인 스타일이 수백 군데 흩어져 있었다. 이걸 전부 걷어 내고 일회용 표식 방식으로 바꾸려면 엄청난 작업이 필요했고, 그 과정에서 멀쩡히 돌아가던 기능이 깨질 위험도 컸다. 게다가 우리는 광고나 외부 위젯 같은 요소도 붙여야 해서, 그런 외부 자원과 엄격한 정책이 충돌하는 문제도 있었다.
그래서 나는 현실적인 절충을 택했다. 인라인 스크립트를 완전히 금지하는 가장 엄격한 단계까지는 당장 가지 못하더라도, 스크립트와 이미지와 통신의 출처를 우리가 신뢰하는 곳으로 제한하는 정책은 확실히 걸었다. 아무 출처나 허용하던 상태에서, 우리가 명시한 곳만 허용하는 상태로 좁힌 것만으로도 방어력이 크게 올라갔다. 완벽한 정책을 나중으로 미루더라도, 지금 할 수 있는 만큼의 제한은 반드시 걸어 두자는 판단이었다.
3. 허용 목록을 관리하는 일
정책을 걸고 나니 새로운 일거리가 생겼다. 우리가 정당하게 쓰는 외부 자원의 출처를 허용 목록에 정확히 넣어 관리하는 일이었다. 예를 들어 방문 분석 도구, 광고, 외부에서 불러오는 글꼴이나 스크립트가 있다면, 그 출처를 정책에 명시해 줘야 브라우저가 막지 않는다. 하나라도 빠뜨리면 멀쩡한 기능이 갑자기 동작하지 않는다.
이게 은근히 까다로웠다. 새로운 외부 서비스를 붙일 때마다 그 서비스가 어떤 출처에서 무엇을 불러오는지 파악해 정책을 갱신해야 했다. 광고 같은 경우는 여러 하위 출처로 자원을 불러와서, 필요한 출처를 빠짐없이 넣는 데 시행착오가 있었다. 정책을 너무 좁게 걸면 정상 기능이 깨지고, 너무 넓게 걸면 방어의 의미가 옅어지니, 딱 필요한 만큼만 여는 세심함이 필요했다.
여기서 얻은 교훈은 정책이 살아 있는 문서라는 것이었다. 한 번 잘 걸고 끝나는 게 아니라, 사이트가 진화할 때마다 함께 손봐야 하는 대상이었다. 그래서 어떤 외부 자원을 왜 허용했는지 기록을 남기고, 더 이상 쓰지 않는 출처는 정책에서 걷어 내 허용 범위를 계속 최소로 유지하려 했다. 방어의 표면적은 좁을수록 좋다는 원칙을 여기서도 지키려 한 것이다.
4. 어긴 것을 관찰하기
콘텐츠 보안 정책에는 유용한 기능이 하나 있었다. 정책을 어기는 시도가 발생하면 그 사실을 보고받을 수 있다는 점이다. 어떤 페이지에서 어떤 자원이 정책에 막혔는지 신호를 모으면, 두 가지를 알 수 있다. 하나는 우리가 허용 목록에 빠뜨린 정상 자원이 있는지, 다른 하나는 실제로 누군가 악성 스크립트를 심으려 시도했는지다.
이 관찰은 정책을 점진적으로 다듬는 데 큰 도움이 됐다. 처음부터 완벽한 정책을 만들 수는 없으니, 일단 걸어 두고 어떤 것이 막히는지 지켜보며 조정하는 방식이다. 정상 자원이 막혔다면 허용 목록에 더하고, 수상한 시도가 잡혔다면 그것대로 대응한다. 방어 장치가 스스로 자신의 상태를 알려 주니, 눈을 감고 규칙을 세우는 게 아니라 데이터를 보며 조율할 수 있었다.
다만 이런 보고 자체도 남용될 수 있어, 보고를 받는 창구가 폭주하지 않도록 하는 배려는 필요했다. 방어를 위해 만든 관찰 통로가 오히려 부담이 되면 곤란하니, 보고의 양을 적절히 조절하는 것도 운영의 일부였다. 어떤 방어 장치든 그 자체가 새로운 공격 표면이 되지 않도록 살피는 습관, 이것도 여러 층을 다루며 몸에 밴 태도였다.
5. 정책이 지키지 못하는 것
콘텐츠 보안 정책은 스크립트의 출처를 통제해 많은 위협을 막지만, 지키지 못하는 영역도 있었다. 대표적인 게 우리가 신뢰해서 허용 목록에 넣은 외부 자원 그 자체가 오염되는 경우다. 우리는 특정 외부 출처의 스크립트를 신뢰해 실행을 허용했는데, 만약 그 외부 출처가 해킹당해 스크립트 내용이 악성으로 바뀌면 어떻게 될까. 정책은 그 출처를 신뢰하도록 설정돼 있으니 순순히 실행해 버린다.
즉 콘텐츠 보안 정책은 어디서 왔느냐는 통제하지만, 그 출처의 내용물이 진짜 우리가 기대한 그것인지는 보장하지 못한다. 신뢰한 출처가 배신하면 정책은 무력하다. 외부의 공용 저장소에서 스크립트나 스타일을 불러다 쓰는 경우, 그 저장소가 침해되면 우리 사이트도 함께 오염되는 위험이 있는 것이다. 출처 통제와 내용 검증은 서로 다른 문제였다.
이 편을 마무리하며 강조하고 싶은 건, 완벽하지 않은 방어라도 걸어 두는 게 안 거는 것보다 훨씬 낫다는 점이었다. 나는 가장 엄격한 정책까지 가지 못한 게 내내 아쉬웠지만, 그렇다고 아무 정책도 안 걸었다면 사이트는 훨씬 취약했을 것이다. 출처를 신뢰하는 곳으로 좁힌 것만으로도 수많은 공격 시나리오가 무력해졌다. 이상에 못 미친다고 손을 놓기보다, 지금 할 수 있는 만큼을 확실히 하고 나머지를 숙제로 남겨 두는 태도가 실제 사이트를 지켰다.
또 하나, 이 정책을 걸며 우리 사이트가 실제로 무엇을 어디서 불러오는지 전체 그림을 처음으로 꼼꼼히 들여다보게 됐다. 방어를 설계하는 과정 자체가 우리 시스템을 더 정확히 이해하는 계기였다. 어떤 외부 자원에 의존하는지, 그중 정말 필요한 건 무엇인지 정리하다 보니, 쓰지 않는데도 남아 있던 자원들을 정리하는 부수적인 소득도 있었다. 방어를 다듬는 일이 곧 시스템을 군더더기 없이 다듬는 일이기도 했다.
그래서 다음 편에서는 이 빈틈을 메우는 방어, 외부 자원의 무결성을 검증하는 방법을 다루려 한다. 불러온 자원이 우리가 기대한 바로 그 내용인지 지문을 대조해 확인하고, 조금이라도 다르면 실행을 거부하는 방식이다. 콘텐츠 보안 정책이 출처를 지킨다면, 이 방법은 내용을 지킨다. 두 방어가 어떻게 손을 잡고 외부 자원의 위험을 함께 막는지를 실제 적용 경험과 함께 이야기하겠다.