앞 편에서 브라우저에 담긴 민감한 데이터가 유출되면 위험하다는 것을 짚었다. 브라우저는 서로 다른 곳에서 온 여러 페이지와 스크립트를 한 화면 안에서 동시에 다룬다. 이때 한 곳의 페이지가 다른 곳의 데이터를 마음대로 넘본다면, 방문한 어떤 페이지가 다른 탭의 은행 정보를 훔쳐볼 수도 있게 된다. 이런 일을 막기 위해 브라우저는 서로 다른 출처의 것들을 철저히 격리하는 보안 모델을 가지고 있다. 이번 편은 그 격리의 뼈대를 이루는 동일 출처 원칙을 다룬다.
이 원칙은 웹 보안의 가장 기초가 되는 울타리다. 이것을 이해하지 못하면, 왜 어떤 요청은 막히고 어떤 데이터는 읽을 수 없는지가 그저 성가신 제약으로만 보인다. 반대로 이 원칙의 취지를 알면, 그 제약이 사용자를 지키기 위한 것임을 이해하고 그 안에서 안전하게 협력하는 길을 찾을 수 있다.
출처라는 경계
브라우저가 격리의 기준으로 삼는 단위는 출처다. 출처는 세 가지가 모두 같을 때에만 같은 것으로 취급된다. 통신 방식을 가리키는 스킴, 서버를 가리키는 호스트, 그리고 연결에 쓰이는 포트다. 이 셋 중 하나라도 다르면 서로 다른 출처로 간주되어, 브라우저는 둘 사이에 울타리를 세운다.
이 기준이 왜 이렇게 정해졌는지는 각 요소가 무엇을 뜻하는지를 보면 이해된다. 스킴이 다르면 하나는 보호되는 연결이고 다른 하나는 보호되지 않는 연결이라, 둘을 같게 취급하면 안전한 쪽이 위험한 쪽에 노출된다. 호스트가 다르면 아예 다른 주체가 운영하는 서버이므로 서로를 신뢰할 근거가 없다. 포트가 다르면 같은 서버라도 다른 서비스일 수 있다.
여기서 주의할 점은 출처가 호스트 전체를 기준으로 하되, 그 안의 경로는 따지지 않는다는 것이다. 같은 호스트 아래의 서로 다른 경로는 같은 출처에 속한다. 그래서 한 출처 안의 페이지들은 서로의 데이터에 접근할 수 있다. 출처는 신뢰의 단위이며, 같은 출처 안은 하나의 신뢰 영역으로 묶인다.
반대로 호스트의 앞부분, 곧 하위 이름이 다르면 다른 출처로 취급된다. 겉보기에 한 조직의 것처럼 보여도, 앞부분이 다르면 브라우저는 별개의 출처로 보아 울타리를 세운다. 그래서 한 조직이 여러 하위 이름으로 나눈 서비스들 사이에서도, 서로 데이터를 나누려면 별도의 허용 절차가 필요하다. 출처의 경계는 눈에 보이는 브랜드가 아니라 이 세 요소의 일치로 정해진다.
다른 출처에는 손대지 못한다
동일 출처 원칙의 핵심은, 한 출처의 스크립트가 다른 출처의 자원과 데이터에 함부로 손대지 못하게 하는 것이다. 한 출처에서 온 스크립트는 같은 출처의 데이터는 자유롭게 다루지만, 다른 출처의 화면 내용을 읽거나 그 저장된 데이터를 꺼내는 것은 막힌다. 이 울타리가 있어 한 페이지가 다른 페이지의 비밀을 넘볼 수 없다.
이 원칙은 특히 여러 페이지가 한 화면 안에 겹쳐 있을 때 중요해진다. 어떤 페이지가 다른 출처의 페이지를 자기 안에 품고 있을 때, 바깥 페이지의 스크립트가 안에 품긴 다른 출처 페이지의 내용을 읽을 수 있다면 큰 위험이 된다. 동일 출처 원칙은 이런 경우에 바깥과 안 사이에 울타리를 세워, 서로 다른 출처끼리는 내용을 넘겨보지 못하게 한다.
데이터 요청에도 이 울타리가 적용된다. 스크립트가 다른 출처의 서버에 데이터를 요청하는 것 자체는 이뤄지더라도, 그 응답을 읽는 것은 기본적으로 막힌다. 요청은 나가되 그 답을 볼 수 없게 하는 것이다. 이렇게 하지 않으면 한 페이지의 스크립트가 사용자의 자격으로 다른 서버에 요청을 보내 그 사용자의 민감한 데이터를 빼낼 수 있기 때문이다.
다만 모든 것이 막히는 것은 아니다. 다른 출처의 이미지를 화면에 띄우거나, 다른 출처의 스크립트를 불러와 실행하는 것처럼, 오래전부터 허용되어 온 몇몇 방식은 열려 있다. 그러나 이런 경우에도 그 내용을 스크립트로 낱낱이 읽는 것은 제한된다. 보이게는 하되 읽지는 못하게 하는 이 미묘한 선이, 편의와 보안 사이의 오랜 절충의 흔적이다.
다른 출처와 안전하게 협력하기
출처를 엄격히 격리하면 안전하지만, 현실의 서비스는 종종 다른 출처와 데이터를 주고받아야 한다. 한 조직이 데이터를 담당하는 서버와 화면을 담당하는 서버를 다른 출처로 나눠 두는 경우가 흔하기 때문이다. 그래서 무조건 막기만 하는 것이 아니라, 서버가 명시적으로 허락하면 다른 출처의 요청도 그 응답을 읽을 수 있게 하는 절차가 마련되어 있다.
이 절차의 핵심은 허락의 주체가 데이터를 가진 서버라는 점이다. 다른 출처에서 온 요청에 대해, 그 데이터를 가진 서버가 이 출처에는 응답을 보여줘도 좋다고 명시하면, 브라우저는 그 허락을 확인하고 응답을 읽도록 허용한다. 서버가 허락하지 않으면 브라우저는 응답을 가로막는다. 즉 이 협력은 요청하는 쪽이 아니라 데이터를 내주는 쪽이 통제한다.
상태를 바꿀 수 있는 민감한 요청에는 한층 신중한 절차가 붙는다. 브라우저는 실제 요청을 보내기 전에, 이런 요청을 보내도 되는지를 서버에 미리 묻는 예비 확인을 먼저 보낸다. 서버가 이 예비 확인에 좋다고 답해야 실제 요청이 나간다. 이 한 단계 덕분에, 허락받지 않은 위험한 요청은 서버에 실제로 닿기 전에 걸러진다.
이 협력 절차를 이해하면, 다른 출처의 데이터를 받으려다 막히는 상황의 원인이 분명해진다. 그것은 대개 데이터를 가진 서버가 이 출처에 허락을 내주지 않았기 때문이다. 해법은 요청하는 쪽을 뜯어고치는 것이 아니라, 데이터를 가진 서버가 올바른 허락을 내주도록 하는 것이다. 허락의 주체가 누구인지를 알면 문제를 엉뚱한 쪽에서 찾지 않는다.
남의 손을 빌린 공격
동일 출처 원칙이 막으려는 위협 가운데 대표적인 것이 사용자의 손을 몰래 빌리는 공격이다. 사용자가 어떤 서비스에 로그인해 있는 상태를 악용해, 사용자도 모르게 그 서비스에 요청을 보내게 만드는 방식이다. 사용자의 브라우저는 로그인 표식을 자동으로 실어 보내므로, 요청 자체는 정상적인 사용자의 것처럼 보인다.
이 공격은 사용자가 악의적인 다른 페이지를 방문했을 때 일어난다. 그 페이지가 사용자 몰래 다른 서비스로 요청을 보내면, 브라우저는 그 서비스의 로그인 표식을 자동으로 붙여 보내 마치 사용자가 직접 한 것처럼 처리되게 만든다. 사용자는 아무것도 하지 않았는데 자기 계정에서 어떤 조작이 일어나는 것이다. 자동으로 실려 가는 표식의 편의가 여기서 약점이 된다.
이 위협을 막는 방법은 여러 겹이다. 표식이 다른 출처에서 시작된 요청에는 실리지 않도록 제한하면, 남의 페이지가 시작한 요청에는 표식이 붙지 않아 사용자의 것으로 위장하지 못한다. 또 상태를 바꾸는 요청에는 그 요청이 정말 우리 화면에서 시작되었음을 증명하는 별도의 표를 요구해, 바깥에서 위조한 요청을 걸러낸다.
또 다른 대표적 위협은 페이지에 악의적인 스크립트가 스며드는 것이다. 사용자가 입력한 내용을 걸러내지 않고 그대로 화면에 넣으면, 그 안에 숨은 스크립트가 페이지의 권한으로 실행되어 데이터를 훔치거나 조작한다. 이를 막으려면 사용자가 넣은 내용을 화면에 뿌리기 전에 위험한 부분을 무력화하고, 실행해도 되는 스크립트의 출처를 미리 제한해 두어야 한다. 격리의 울타리 안에서도 이런 침투를 함께 막아야 보안이 완성된다.
강한 기능에 붙는 권한
브라우저는 격리의 울타리에 더해, 강력한 기능에는 사용자의 명시적 허락을 요구하는 또 하나의 장치를 둔다. 사용자의 위치를 알아내거나, 카메라와 마이크를 켜거나, 알림을 보내는 것처럼 오용되면 사생활을 침해할 수 있는 기능들이 여기에 해당한다. 이런 기능은 페이지가 원한다고 곧바로 쓸 수 있는 것이 아니라, 사용자가 허락해야 비로소 열린다.
이 허락은 사용자가 그 순간에 직접 내리는 것이라야 의미가 있다. 그래서 브라우저는 페이지가 이런 기능을 요청하면 사용자에게 물어, 허락과 거부를 사용자가 정하게 한다. 사용자가 거부하면 페이지는 그 기능을 쓸 수 없다. 이 물음이 사용자도 모르게 그의 위치나 카메라가 쓰이는 것을 막는 최후의 방벽이다.
이런 강한 기능은 대개 안전한 연결 위에서만 열린다. 보호되지 않는 연결에서는 중간에서 그 기능의 결과가 가로채이거나 조작될 수 있어, 위치나 영상 같은 민감한 데이터를 다루기에 위험하기 때문이다. 앞서 여러 편에서 본 안전한 연결의 요구가 여기서도 강한 기능의 전제로 이어진다. 안전한 맥락이 아니면 강한 능력을 주지 않는 것이다.
허락을 요청하는 방식에도 배려가 필요하다. 페이지에 들어서자마자 맥락 없이 여러 권한을 한꺼번에 요구하면, 사용자는 영문을 몰라 거부하거나 불신하게 된다. 그 기능이 실제로 필요해지는 순간에, 왜 필요한지가 자연스럽게 드러나는 자리에서 요청해야 사용자가 납득하고 허락한다. 권한 요청은 기능의 문제일 뿐 아니라 신뢰의 문제다.
사용자가 한번 내린 허락과 거부는 대개 기억되지만, 사용자는 언제든 그 결정을 바꿀 수 있다. 그래서 페이지는 사용자가 거부했거나 나중에 거둬들인 상황에서도 무너지지 않게 대비해야 한다. 그 기능이 없으면 아예 쓸 수 없는 페이지보다, 없으면 없는 대로 기본 기능은 돌아가는 페이지가 사용자에게 안전하다. 강한 기능은 있으면 좋은 것이되, 그것에 전부를 걸지 않는 설계가 견고하다.
이 권한의 장치는 앞서 다룬 출처의 격리와 같은 정신을 공유한다. 격리가 서로 다른 출처끼리 데이터를 넘보지 못하게 막는다면, 권한은 페이지가 사용자의 동의 없이 강한 기능을 쓰지 못하게 막는다. 둘 다 페이지의 힘에 한계를 그어 사용자를 보호한다는 점에서 하나의 원칙 아래 있다. 브라우저의 보안은 이렇게 여러 층위에서 페이지의 능력을 필요한 만큼으로 제한한다.
이 제한을 성가시게 여겨 우회하려 하기보다, 사용자를 지키기 위한 최소한의 힘만 요구하는 태도가 옳다. 정말 필요한 기능만, 정말 필요한 순간에 요청하고, 사용자가 거부해도 무너지지 않게 대비하면, 그 페이지는 사용자에게 신뢰를 얻는다. 강한 기능을 절제해서 다루는 것 자체가 사용자를 존중하는 설계다.
격리를 이해하고 다루기
지금까지의 이야기를 종합하면, 브라우저의 보안 모델은 서로 다른 출처를 신뢰의 단위로 나누고 그 사이에 울타리를 세우는 데서 출발한다. 이 울타리는 한 페이지가 다른 페이지의 데이터를 넘보지 못하게 하고, 사용자의 손을 몰래 빌리는 공격을 어렵게 만든다. 여러 곳에서 온 것들이 한 화면에 뒤섞이는 웹에서, 이 격리가 없으면 어떤 안전도 성립하지 않는다.
이 울타리를 성가신 제약으로만 여기면 우회하려는 유혹에 빠진다. 그러나 제약을 억지로 뚫으려 하면 그 과정에서 스스로 구멍을 낸다. 올바른 태도는 울타리의 취지를 이해하고, 다른 출처와 협력해야 할 때는 정해진 허락 절차를 통해 안전하게 여는 것이다. 보안은 울타리를 없애는 것이 아니라 필요한 곳에 정해진 문을 내는 일이다.
보안을 다룰 때 특히 유념할 것은, 안전이 여러 겹의 방어가 쌓여 만들어진다는 점이다. 출처의 격리 하나만으로 모든 위협이 막히지는 않는다. 표식의 제한, 요청의 진위 확인, 입력 내용의 무력화, 실행 스크립트 출처의 제한이 함께 갖춰져야 촘촘한 방어가 된다. 어느 한 겹만 믿고 나머지를 소홀히 하면 그 소홀한 틈이 공격의 통로가 된다.
정리하면, 브라우저는 스킴과 호스트와 포트가 모두 같을 때에만 같은 출처로 보아 신뢰 영역을 묶고, 다른 출처 사이에는 데이터 접근을 막는 울타리를 세우며, 협력이 필요할 때는 데이터를 가진 서버의 허락으로 그 문을 연다. 이 격리 위에 여러 겹의 방어를 더해야 사용자의 데이터가 지켜진다. 다음 편에서는 이 보안의 틀 안에서 페이지를 프록시처럼 가로채 오프라인 동작까지 가능하게 하는 서비스 워커를 다룬다.