교차 출처 자원 공유(Cross-Origin Resource Sharing, CORS)는 웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으로부터 요청할 수 있게 허용하는 구조를 의미합니다.
쉽게 말하자면, CORS는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제라고 말할 수 있습니다.
또한 CORS는 교차 출처 요청을 허용하는 것이 안전한지 안전하지 않은지를 판별하기 위해 브라우저와 서버가 상호 통신하는 하나의 방법을 정의합니다.
웹 개발자들이 겪는 CORS 관련 이슈는 모두 CORS 정책을 위반했기 때문에 발생하는 것들입니다. 개발하는 입장에서는 정책 때문에 신경써야 하는 것들이 늘어나니 귀찮을 수도 있지만, 사실 CORS라는 방어막이 존재하기 때문에 이 곳 저 곳에서 가져오는 리소스가 안전하다는 최소한의 보장을 받을 수 있는 것입니다.
CORS는 'Cross-Origin Resource Sharing'의 줄임말로, 한국어로 직역하면 '교차 출처 리소스 공유'라고 해석할 수 있습니다. 여기서 '교차 출처'라고 하는 것은 '다른 출처'를 의미하는 것으로 생각하시면 됩니다.
교차 출처간의 리소스 공유에 대해서 알아보기에 앞서 간단하게 '출처(Origin)'라는 단어가 정확히 무엇을 의미하는지 살펴보겠습니다.
서버의 위치를 의미하는 https://www.naver.com:443 같은 URL들은 마치 하나의 문자열 같아 보여도, 사실은 여러 개의 구성 요소로 이루어져있습니다.
이때 출처는 프로토콜(Protocol)과 호스트(Host) 그리고 포트(Port) 번호까지 모두 합친 것을 의미합니다. 즉, 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것입니다. 또한 출처 내의 포트 번호는 생략이 가능한데, 이는 각 웹에서 사용하는 HTTP, HTTPS 프로토콜의 기본 포트 번호가 정해져있기 때문입니다.
※ 기본 포트 번호
HTTP: 80
HTTPS: 443
그러나 만약 https://www.naver.com:443과 같이 출처에 포트 번호가 명시적으로 포함되어 있다면, 이 포트 번호까지 모두 일치해야 같은 출처라고 인정됩니다.
참고로 브라우저 개발자 도구의 콘솔에서 Location 객체가 가지고 있는 origin 프로퍼티에 접근함으로써 손 쉽게 애플리케이션이 실행되고 있는 출처를 알아낼 수 있습니다.
// 브라우저 콘솔 JavaScript 코드
console.log(location.origin);
// 실행결과
https://www.naver.com
웹 생태계에는 교차 출처로의 리소스 요청을 제한하는 것과 관련된 2가지 정책이 존재합니다. 한 가지는 교차 출처 자원 공유(Cross-Origin Resource Sharing, CORS), 그리고 또 한 가지는 동일 출처 정책(Same-Origin Policy, SOP)입니다.
동일 출처 정책(same-origin policy)은 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. 동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.
쉽게 말하자면, 동일 출처 정책은 '같은 출처에서만 리소스를 공유할 수 있다'라는 규칙을 가진 정책입니다.
그러나 웹이라는 오픈스페이스 환경에서 다른 출처에 있는 리소스를 가져와서 사용하는 일은 굉장히 흔한 일이라 무작정 막을 수도 없는 노릇이니 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 출처가 다르더라도 허용하기로 했는데, 그 중 하나가 'CORS 정책을 지킨 리소스 요청'인 것입니다.
우리가 다른 출처로 리소스를 요청한다면 SOP 정책을 위반한 것이 되고, 게다가 SOP의 예외 조항인 CORS 정책까지 지키지 않는다면, 다른 출처의 리소스를 사용할 수 없게 되는 것입니다.
출처가 다른 2개의 애플리케이션이 자유롭게 소통할 수 있는 환경은 꽤 위험한 환경입니다. 애초에 클라이언트 애플리케이션, 특히나 웹에서 돌아가는 클라이언트 애플리케이션은 사용자의 공격에 너무나도 취약합니다. 당장 브라우저의 개발자 도구만 열어도 DOM이 어떻게 작성되어있는지, 어떤 서버와 통신하는지, 리소스의 출처는 어디인지와 같은 각종 정보들을 아무런 제재없이 열람이 가능합니다.
자바스크립트 소스 코드가 난독화 되지 않아 개발자 도구만 열면 'script' 태그 안에 소스 코드가 그대로 노출되어 보안적으로 상당히 취약한 사이트들도 많습니다.
이런 상황 속에서 다른 출처의 애플리케이션이 서로 통신하는 것에 대해 아무런 제약도 존재하지 않는다면, 악의를 가진 사용자가 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting)와 같은 방법을 사용하여 우리들의 애플리케이션에서 코드가 실행된 것처럼 꾸며 사용자의 정보를 탈취하기가 너무나도 쉬워집니다.
그렇다면 정확히 어떤 경우에 출처가 같다고 판단하고, 어떤 경우에 출처가 다르다고 판단하는 것일까요?
사실 2개의 출처가 서로 같다고 판단하는 로직 자체는 굉장히 간단한데, 두 URL의 구성 요소 중 Protocol, Host, Port, 이 3가지만 동일하면 됩니다. 즉, 같은 프로토콜(Protocol), 호스트(Host), 포트(Port)를 사용하고 있다면 나머지는 전부 다르더라도 같은 출처로 인정이 된다는 것입니다. 반대로, 웹 애플리케이션은 리소스가 자신의 출처(Protocol, Host, Port)와 다를 경우, 브라우저는 교차 출처 HTTP 요청을 실행합니다.
여기서 중요한 사실 한 가지는 이렇게 출처를 비교하는 로직이 서버에 구현된 스펙이 아니라 브라우저에 구현되어 있는 스펙이라는 것입니다. 만약 우리가 CORS 정책을 위반하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면, 서버는 정상적으로 응답을 하고, 이후 브라우저가 이 응답을 분석해서 CORS 정책 위반이라고 판단되면, 그 응답을 사용하지 않고 그냥 버리는 순서인 것입니다.
즉, CORS는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않습니다. 또한 CORS 정책을 위반하는 리소스 요청 때문에 에러가 발생했다고 해도 서버 쪽 로그에는 정상적으로 응답을 했다는 로그만 남기 때문에, CORS가 돌아가는 방식을 정확히 모르면 에러(Error) 추적에 난항을 겪을 수도 있습니다.