CORS(Cross Origin Resource Sharing)란?
Origin의 의미
CORS에서의 Origin(출처)는 URL에서 프로토콜, 도메인, 포트 번호를 합친 부분을 의미한다.
동일 출처 정책(SOP)이란?
SOP(Single Origin Policy, 동일 출처 정책)은 다른 Origin으로 요청을 보낼 수 없도록 금지하는 브라우저의 기본적인 보안 정책이다.
즉, 동일한 Origin으로만 요청을 보낼 수 있게 하는 정책이다. 만약 SOP가 없어서 자유롭게 다른 Origin으로 요청을 보낸다면 어떻게 될까?
아래와 같은 CSRF(Cross Site Request Forgery) 공격 상황을 가정해보자.
악의적인 마음을 품은 해커가 웹 사이트를 구축하였다.
해커는 해당 웹 사이트와 연결된 링크를 사용자들에게 보낸다.
사용자는A라는 웹 사이트에 로그인되어 있고, 사용자의 인증 정보가 브라우저에 존재한다.
사용자가실수로 해커가 보낸 링크를 클릭하여 접속한다.
해커가 심어둔 코드가 실행되어 A 웹 사이트로 사용자의 개인 정보 조회 API 요청을 보낸다.
사용자의 브라우저에 인증 정보가 존재하기에 이것이 요청과 함께 전송된다.
서버는 인증된 요청이라 생각하여 개인 정보를 응답해준다.
해커는 개인 정보를 사용할 수 있게 된다.
그런데 SOP이 존재한다면 해커가 구축한 웹 사이트와 A 웹 사이트의 Origin이 다르기 때문에,
해커의 웹 사이트에서 A 웹 사이트의 서버로 API 요청을 보낼 수 없다.
이와 같은 SOP는 과거 절대적인 규칙이었다. 그러나 기술이 발전하면서 서로 다른 Origin끼리 데이터를 주고받아야 하는 일이 많아졌다.
이로 인해 SOP는 별도의 예외 사항을 두어 다른 Origin으로도 요청을 보낼 수 있게 하였다.
CORS(Cross Origin Resource Sharing)가그 예외 사항에 해당한다.
교차 출처 자원 공유(CORS) 정책이란?
HTTP 헤더를 사용하여 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있도록 권한을 부여하는 정책을 의미한다.
웹 애플리케이션은 리소스가 자신의 출처(프로토콜, 도메인, 포트번호)와 다를 때 교차 출처 HTTP 요청을 실행한다.
기본적으로 웹 애플리케이션은 다른 출처의 자원을 요청할 때, HTTP 프로토콜을 사용하여 요청한다.
이때 접근할 수 있는 권한을 부여받기 위하여, 요청 헤더 Origin 필드에 요청을 보내는 출처를 담아 전송한다.
서버는 요청에 대한 응답 시, 응답 헤더에 Access-Control-Allow-Origin 필드에 접근이 허용된 출처들을 담아 전송한다.
응답을 받은 웹 애플리케이션은 요청 헤더의 Origin과 서버가 보내준 응답 헤더의 Access-Control-Allow-Origin을 비교한다.
Origin의 출처가 Access-Control-Allow-Origin에서 허용된 출처일 때, 자원 접근이 가능하다.
CORS(Cross Origin Resource Sharing)는 다른 Origin으로 요청을 보내기 위해 지켜야 하는 정책으로,
원래대로라면 SOP에 의해 막혀야 하는 요청을 풀어주는 정책이라고 할 수 있다.
CORS 동작 원리와 요청 방식
CORS의 기본 동작 원리는 단순하다. 브라우저는 다른 Origin으로 요청을 보낼 때, Origin 헤더에 자신의 Origin을 설정하고
서버로부터 응답을 받으면 응답의 Allow-Control-Allow-Origin 헤더에 설정된 Origin 목록에서 요청 시 보냈던 Origin 헤더 값이
포함되는지 검사한다. CORS 요청은 아래와 같이 세 가지 유형으로 나누어 생각해볼 수 있다.
단순 요청(Simple Request)
단순 요청(Simple Request)은 공식 용어가 아닌, MDN에서 사용한 용어를 가져온 것이다. 그런데 이름이 의미하는 것과 달리,
단순 요청은 흔한 유형의 요청이 아니다. 단순 요청이 되기 위한 조건이 매우 까다롭기 때문이다. 그 조건은 아래와 같다.
- 메서드가 GET, HEAD, POST 중 하나여야 한다.
- User Agent가 자동으로 설정한 헤더를 제외하면, 아래와 같은 헤더들만 사용할 수 있다.
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink (en-US)
- Save-Data
- Viewport-Width
- Width
- Content-Type 헤더에는 아래와 같은 값들만 설정할 수 있다.
- application/x-www-form-urlencode
- multipart/form-data
- text/plain
- . . .
위와 같은 조건을 만족하는 단순 요청은 안전한 요청으로 취급되어, 프리플라이트 요청이 필요 없이 단 한 번의 요청만을 전송한다.
위에서 언급한 CORS의 기본적인 동작 원리를 그대로 따른다. 다른 Origin으로 요청을 보낼 때 Origin 헤더에 자신의 Origin을 설정하고,
서버로부터 응답을 받으면 응답의 Allow-Control-Allow-Origin 헤더에 설정된 Origin의 목록에 요청의 Origin 헤더 값이 포함되는지
검사하는 것이다. 이를 그림으로 나타내면 다음과 같다.
프리플라이트 요청 (Preflight Request)
위에서 소개한 단순 요청의 조건에 벗어나는(= 안전하지 않은) 요청의 경우, 서버에 실제 요청을 보내기 전에 예비 요청에 해당하는
프리플라이트 요청(Preflight Request)을 먼저 보내서 실제 요청이 전송하기에 안전한지 확인한다.
만약 안전한 요청이라고 확인이 된다면, 그때서야 실제 요청을 서버에게 보낸다.
따라서 총 두 번의 요청을 전송한다.
프리플라이트 요청의 특징은 다음과 같다.
- 메서드로 OPTIONS를 사용한다.
- Origin 헤더에 자신의 Origin을 설정한다.
- Access-Control-Request-Method 헤더에 실제 요청에 사용할 메서드를 설정한다.
- Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정한다.
서버는 이러한 프리플라이트 요청에 대해 다음과 같은 특징을 가진 응답을 제공해야 한다.
- Access-Control-Allow-Origin 헤더에 허용되는 Origin들의 목록 혹은 와일드카드(*)를 설정한다.
- Access-Control-Allow-Methods 헤더에 허용되는 메서드들의 목록 혹은 와일드카드(*)를 설정한다.
- Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록 혹은 와일드카드(*)를 설정한다.
- Access-Control-Max-Age 헤더에 해당 프리플라이트 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정한다.
이러한 응답을 받고 나면 브라우저는 이 응답의 정보를 자신이 전송한 요청의 정보와 비교하여 실제 요청의 안전성을 검사한다.
만약 이 안전성 검사에 통과하게 된다면, 그때서야 실제 요청을 서버에게 보낸다.
단, 이때는 Access-Control-Request-XXX 형태의 헤더는 보내지 않는다.
예를 들어, Content-Type 헤더의 값이 application/json이고 사용자 정의 헤더로 Custom-Header를 사용하는 POST 요청을
서버에게 보내려 한다고 해보자. 그러면 이는 단순 요청의 조건에 벗어나기 때문에 프리플라이트 요청이 필요하다.
따라서 총 두 번의 요청이 서버에게 전송되며, 이를 그림으로 나타내면 다음과 같다.
인증 정보를 포함한 요청 (Credentialed Request)
위에서 소개한 두 유형의 요청은 인증 정보가 없는 경우였다.
여기서 말하는 인증 정보(Credential)란 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등을 일컫는다.
만약 이러한 인증 정보를 함께 보내야 하는 요청(Credentialed Request)이라면, 별도로 따라줘야 하는 CORS 정책이 존재한다.
이에 대해 한 번 알아보자.
우선, 쿠키 등의 인증 정보를 보내기 위해서는 클라이언트 단에서 요청 시 별도의 설정이 필요하다.
이는 Ajax 요청을 위해 어떠한 도구를 사용하느냐에 따라 달라진다. 만약 XMLHttpRequest, jQuery의 ajax, 또는 axios를 사용한다면 withCredentials 옵션을 true로 설정해줘야 한다. 반면, fetch API를 사용한다면 credentials 옵션을 include로 설정해줘야 한다.
이러한 별도의 설정을 해주지 않으면 쿠키 등의 인증 정보는 절대로 자동으로 서버에게 전송되지 않는다.
위와 같은 설정을 통해 인증 정보를 요청에 포함시켰다면, 이 요청은 이제 인증 정보를 포함한 요청이 된다.
그리고 서버는 이러한 요청에 대해 일반적인 CORS 요청과는 다르게 대응해줘야 한다.
응답의 Allow-Control-Allow-Origin 헤더가 와일드카드(*)가 아닌 분명한 Origin으로 설정되어야 하고,
Allow-Control-Allow-Credentials 헤더는 true로 설정되어야 한다.
그렇지 않으면 브라우저에 의해 응답이 거부된다. 이를 그림으로 나타내면 다음과 같다.