다른 도메인의 데이터를 불러오려다 브라우저 콘솔에서 붉은 오류를 만난 경험은 웹을 다루는 사람이라면 한 번쯤 겪는다. 요청은 분명히 서버에 닿았고 응답도 돌아왔는데, 브라우저가 그 응답을 스크립트에 넘겨주지 않는 상황이다. 이 벽의 정체가 교차 출처 자원 공유, 흔히 부르는 코르스다. 이번 편은 이 규칙이 왜 존재하며 어떻게 동작하는지를 처음부터 끝까지 다룬다.
코르스를 이해하려면 먼저 그 앞에 놓인 동일 출처 정책이라는 브라우저의 기본 방침을 알아야 한다. 코르스는 이 방침에 예외를 여는 장치이기 때문이다. 이번 편은 동일 출처 정책의 전제에서 출발해, 출처의 정의, 요청이 막히는 이유, 사전 요청의 동작, 서버가 허용을 표현하는 방식, 그리고 오류를 진단하는 방법까지 순서대로 짚는다.
동일 출처 정책이라는 전제
브라우저에는 동일 출처 정책이라는 기본 방침이 있다. 한 출처에서 불러온 페이지의 스크립트는, 원칙적으로 같은 출처의 자원에만 자유롭게 접근할 수 있다는 규칙이다. 다른 출처의 자원에 스크립트로 접근하려는 시도는 기본적으로 제한된다. 이 방침은 웹 보안의 근간을 이루는 오래된 원칙이다.
이 정책이 없다면 어떤 일이 벌어질지 생각해 보면 그 필요성이 분명해진다. 사용자가 악성 페이지를 열었을 때, 그 페이지의 스크립트가 사용자가 로그인해 둔 다른 사이트의 데이터를 마음대로 읽어 갈 수 있다면 큰 위험이다. 동일 출처 정책은 이런 무단 접근을 브라우저 차원에서 막아, 서로 다른 사이트가 서로의 데이터를 함부로 넘보지 못하게 한다.
중요한 점은 이 제한이 서버가 아니라 브라우저의 동작이라는 것이다. 요청 자체는 서버로 나가고 응답도 돌아오지만, 브라우저가 그 응답을 스크립트에 넘겨줄지 말지를 정책에 따라 판단한다. 그래서 코르스 문제는 서버가 응답을 안 주는 문제가 아니라, 브라우저가 응답을 스크립트에 전달하지 않는 문제인 경우가 많다.
동일 출처 정책은 지나치게 엄격해 보이지만, 현실의 웹은 서로 다른 출처 사이의 통신을 끊임없이 필요로 한다. 하나의 화면이 여러 곳의 데이터를 모아 보여 주는 일이 흔하기 때문이다. 그래서 이 엄격한 기본 방침에 안전하게 예외를 여는 표준이 필요했고, 그것이 코르스다. 코르스는 정책을 무너뜨리는 것이 아니라, 정책 아래에서 통제된 예외를 허용하는 장치다.
출처란 무엇인가
코르스를 이해하려면 출처라는 개념을 정확히 알아야 한다. 출처는 세 가지 요소의 조합으로 정해진다. 통신 방식과 도메인 이름, 그리고 포트 번호다. 이 셋이 모두 같아야 같은 출처이고, 하나라도 다르면 다른 출처로 취급된다.
이 정의는 직관과 어긋날 때가 있다. 같은 도메인이라도 통신 방식이 암호화된 것과 아닌 것이 다르면 서로 다른 출처다. 도메인이 같아도 앞에 붙은 하위 이름이 다르면 역시 다른 출처로 본다. 포트 번호가 다른 경우도 마찬가지다. 겉보기에 비슷해 보여도 이 세 요소 중 하나가 어긋나면 교차 출처가 된다.
이 엄격한 정의 때문에 개발 중에 혼란이 자주 생긴다. 같은 회사의 서비스인데도 도메인 앞자리가 다르면 브라우저는 남남으로 취급한다. 화면을 그리는 쪽과 데이터를 주는 쪽의 출처가 다르면, 아무리 같은 조직이 운영하더라도 코르스의 대상이 된다. 이 사실을 모르면 왜 우리 서비스끼리 통신이 막히는지 이해하지 못한다.
출처의 정의를 정확히 알면 문제의 원인을 빠르게 짚을 수 있다. 요청을 보내는 페이지의 출처와 응답을 주는 서버의 출처를 나란히 놓고 세 요소를 비교하면, 이것이 교차 출처 상황인지 아닌지가 곧바로 드러난다. 코르스 오류를 만났을 때 가장 먼저 확인해야 할 것이 바로 이 두 출처의 비교다.
교차 출처 요청이 막히는 이유
교차 출처 요청이 무조건 막히는 것은 아니다. 화면에 이미지를 불러오거나 다른 곳의 스크립트를 실행하는 것처럼, 오래전부터 허용되어 온 교차 출처 동작도 많다. 문제가 되는 것은 주로 스크립트가 다른 출처의 응답 내용을 읽어 가려는 경우다. 브라우저는 이런 읽기를 기본적으로 차단한다.
차단의 논리는 응답의 내용을 보호하는 데 있다. 요청을 보내는 것 자체보다, 그 응답에 담긴 데이터를 다른 출처의 스크립트가 읽어 가는 것이 위험하기 때문이다. 사용자의 권한으로 돌아온 응답을 악성 페이지가 읽어 갈 수 있다면, 민감한 정보가 그대로 새어 나간다. 그래서 브라우저는 서버가 명시적으로 허락하지 않는 한 그 읽기를 막는다.
여기서 핵심은 허락의 주체가 서버라는 점이다. 브라우저는 서버가 이 출처에 응답 내용을 보여 줘도 좋다고 명시했는지를 확인한다. 서버가 특정 헤더로 허용의 뜻을 밝히면 브라우저는 응답을 스크립트에 넘기고, 그런 표시가 없으면 응답을 차단한다. 결국 교차 출처 접근을 허락할지는 자원을 가진 서버가 결정하는 것이다.
이 구조를 이해하면 코르스를 대하는 방향이 잡힌다. 클라이언트 쪽 코드를 아무리 고쳐도 서버가 허용을 표현하지 않으면 문제는 풀리지 않는다. 코르스는 브라우저와 서버가 함께 만드는 규칙이고, 허용의 열쇠는 서버가 쥐고 있다. 그래서 코르스 설정은 대부분 서버 쪽에서 이루어진다.
한 가지 자주 오해되는 지점은 코르스가 서버를 보호하는 장치라는 생각이다. 코르스는 서버가 아니라 사용자를 보호한다. 서버를 향한 접근을 막고 싶다면 그것은 인증과 권한의 몫이지 코르스의 몫이 아니다. 코르스는 오히려 서버가 특정 출처에 응답을 열어 주도록 허용의 문을 여는 쪽에 가깝다. 이 방향을 거꾸로 이해하면, 코르스로 서버를 지키려다 엉뚱한 설정을 하게 된다. 보호의 주체와 대상을 정확히 구분하는 것이 코르스를 오해 없이 다루는 출발점이다.
단순 요청과 사전 요청
교차 출처 요청은 그 성격에 따라 두 갈래로 처리된다. 하나는 곧바로 보내지는 단순 요청이고, 다른 하나는 본 요청에 앞서 확인 절차를 먼저 거치는 요청이다. 이 둘을 가르는 기준은 요청이 서버의 상태를 바꿀 위험이 있는지, 그리고 특별한 헤더나 형식을 쓰는지다.
조회처럼 부작용이 적고 평범한 형식을 쓰는 요청은 단순 요청으로 분류되어 곧바로 나간다. 브라우저는 요청을 보내면서 어느 출처에서 시작되었는지를 헤더에 담고, 돌아온 응답에 허용 표시가 있는지를 확인한다. 표시가 있으면 응답을 넘기고 없으면 차단하는 사후 검사 방식이다.
반면 상태를 바꿀 수 있거나 특별한 헤더를 쓰는 요청은 본 요청을 보내기 전에 확인 요청을 먼저 보낸다. 이 사전 요청은 브라우저가 자동으로 보내며, 서버에게 이런 방식과 이런 헤더로 요청을 보내도 되는지를 미리 묻는다. 서버가 허용한다고 답하면 그제야 본 요청이 나가고, 허용하지 않으면 본 요청은 아예 보내지지 않는다.
사전 요청은 안전을 위한 장치다. 서버의 상태를 바꿀 수 있는 요청이 무턱대고 나가기 전에, 서버가 그것을 받아들일 준비가 되어 있는지를 확인하는 것이다. 이 과정이 한 번의 왕복을 더하므로 약간의 지연이 생기지만, 브라우저는 그 허용 결과를 일정 기간 저장해 두어 같은 요청이 반복될 때는 사전 요청을 생략한다.
서버가 허용을 표현하는 방식
서버는 응답에 특정 헤더를 담아 교차 출처 접근을 허용한다는 뜻을 밝힌다. 가장 기본이 되는 것은 어느 출처의 접근을 허용하는지를 명시하는 헤더다. 이 헤더에 요청을 보낸 출처를 담아 주면, 브라우저는 그 출처의 스크립트에 응답을 넘긴다. 모든 출처를 허용하는 표시를 담을 수도 있지만, 이는 신중하게 써야 한다.
사전 요청에 대한 응답에서는 허용하는 메서드와 헤더도 함께 밝힌다. 어떤 메서드로 본 요청을 보내도 되는지, 어떤 헤더를 실어도 되는지를 서버가 명시하면, 브라우저는 그 범위 안에서만 본 요청을 허용한다. 클라이언트가 쓰려는 메서드나 헤더가 이 허용 목록에 없으면 본 요청은 막힌다.
허용의 범위를 얼마나 넓게 열지는 보안의 문제다. 모든 출처를 허용하면 편리하지만, 그만큼 아무 사이트나 우리 서버의 응답을 읽을 수 있게 된다. 공개된 정보라면 넓게 열어도 되지만, 사용자별 데이터를 다루는 서버라면 허용 출처를 명확히 지정해야 한다. 편의를 위해 무작정 넓게 여는 설정은 위험을 부른다.
서버가 허용 헤더를 정확히 구성하는 것이 코르스 해결의 핵심이다. 오류의 대부분은 서버가 필요한 허용 헤더를 빠뜨렸거나, 요청한 출처를 허용 목록에 넣지 않았거나, 사전 요청에 제대로 답하지 않은 데서 나온다. 클라이언트가 아니라 서버의 응답 헤더를 살피는 것이 진단의 지름길인 이유다.
자격 증명이 걸린 요청
쿠키 같은 자격 증명을 함께 보내는 교차 출처 요청은 규칙이 더 엄격하다. 기본적으로 브라우저는 교차 출처 요청에 자격 증명을 싣지 않는다. 자격 증명을 실으려면 클라이언트가 명시적으로 그럴 의사를 밝혀야 하고, 서버도 자격 증명이 담긴 요청을 허용한다는 별도의 표시를 응답에 담아야 한다.
이때 중요한 제약이 하나 붙는다. 자격 증명이 걸린 요청에서는 모든 출처를 허용하는 표시를 쓸 수 없다. 서버는 반드시 요청을 보낸 구체적인 출처를 명시해야 한다. 자격 증명이 실린 요청은 사용자의 권한으로 처리되므로, 아무 출처에나 그 응답을 열어 주면 위험하기 때문이다. 이 제약을 모르면 왜 설정이 동작하지 않는지 한참을 헤맨다.
이 규칙은 편의와 안전 사이의 균형에서 나온 것이다. 자격 증명이 실리지 않은 요청은 비교적 관대하게 다루어도 되지만, 사용자의 신분이 실린 요청은 훨씬 조심스럽게 다루어야 한다. 그래서 자격 증명이 걸리는 순간, 허용의 범위를 넓게 여는 지름길이 막히고 구체적인 명시가 요구된다.
실무에서는 자격 증명이 필요한지 아닌지를 먼저 분명히 해 두는 것이 좋다. 자격 증명이 필요 없다면 요청에 그것을 싣지 않아 규칙을 단순하게 유지하고, 정말로 필요하다면 클라이언트와 서버 양쪽에 필요한 표시를 빠짐없이 갖추어야 한다. 이 부분을 어중간하게 두면 원인을 찾기 어려운 오류가 생긴다.
CORS 오류를 진단하는 법
코르스 오류를 만났을 때 첫걸음은 그것이 정말 코르스 문제인지 확인하는 것이다. 브라우저 콘솔의 오류 메시지가 출처 관련 차단을 가리키는지, 아니면 다른 종류의 오류인지를 먼저 구분한다. 네트워크 탭에서 요청이 서버에 닿아 응답이 돌아왔는데도 스크립트가 그것을 읽지 못한다면 코르스 문제일 가능성이 높다.
다음으로 요청의 출처와 서버의 출처를 비교해 교차 출처 상황인지 확인한다. 그리고 서버의 응답 헤더에 허용 표시가 담겨 있는지, 담겨 있다면 요청한 출처와 일치하는지를 살핀다. 사전 요청이 발생하는 요청이라면 그 사전 요청에 서버가 제대로 답했는지도 함께 본다. 오류의 원인은 대개 이 확인 과정에서 드러난다.
흔한 착각은 클라이언트 코드를 고쳐 코르스를 우회하려는 것이다. 코르스는 브라우저가 사용자를 보호하기 위해 지키는 규칙이므로, 클라이언트 쪽에서 그것을 무력화하는 정공법은 없다. 해결은 서버가 허용을 올바로 표현하도록 설정하는 데 있다. 우회를 찾기보다 서버의 응답 헤더를 바로잡는 것이 정도다.
이번 편은 동일 출처 정책의 전제에서 시작해 출처의 정의, 요청이 막히는 이유, 사전 요청의 동작, 서버의 허용 표현, 자격 증명의 규칙, 그리고 오류 진단까지 코르스의 전 과정을 다루었다. 코르스가 출처 사이의 접근을 통제하는 규칙이라면, 다음 편에서는 같은 자원을 반복해 받지 않도록 아껴 두는 규칙인 캐시 제어를 살펴본다.