CORS(Cross Origin Resource Sharing)란 브라우저의 보안과 관련하여 아주 중요한 역할을 하는 보안 정책이다. CORS를 이해하기 위해서는 그 바탕이 되는 SOP부터 이해하고 넘어가는 편이 좋다.
CORS와 SOP 모두 Origin이라는 개념을 포함하고 있는데, 여기서 Origin은 한국어로 '출처'로 번역된다. 일반적으로는 URL, 도메인 등과 유사한 개념으로 사용되지만, 여기서의 Origin은 의미가 다르기 때문에 명확히 이해하고 넘어가야 한다.
정리하자면 Origin은 URL에서 프로토콜, 도메인, 포트 번호를 합친 부분
을 의미한다. 예를 들어 다음과 같은 URL이 있다고 하자.
https://velog.io:443/@mino0121/series/CS-Algorithm
여기서 프로토콜(혹은 Scheme)은 https://
이고 도메인에 해당하는 부분은 velog.io
이며 포트 번호는 :443
이다. 따라서 Origin은 https://velog.io:443
이다.
그렇다면 브라우저의 보안 정책 중 하나인 SOP는 무엇일까? SOP는 '동일 출처 정책'으로 번역 가능한데, '다른 Origin으로 요청을 보낼 수 없도록 금지하는 브라우저의 기본 보안 정책'이다. 즉, 브라우저에서 요청을 보낼 때 동일한 origin으로만 요청을 보낼 수 있도록 하는 것이다.
간단한 CSRF 공격을 상정해 보자. 공격자가 웹사이트 혹은 게시글을 만들고 사용자가 이를 클릭하면 악성 스크립트가 실행되어 사용자가 자신의 개인 정보를 조회하는 API 요청을 보내는 식이다. 사용자가 A라는 웹사이트에 로그인한 상태에서 해당 스크립트가 실행된다면, 이 사용자의 브라우저에는 인증 정보가 존재하므로 이 인증 정보를 해당 요청(악의적인 요청)에 실어서 보내게 되고, A 웹사이트는 이를 인증된 요청으로 받아들여 개인 정보를 조회한 뒤 응답할 것이다. 그리고 그 정보는 공격자가 탈취할 수 있게 된다.
이때 만약 다른 Origin으로의 요청을 막는 SOP 정책이 있다면 이러한 문제를 어느 정도 방지할 수 있다. 공격자가 구축한 가짜 웹사이트는 엄연히 A와는 다른 웹사이트이므로 해커의 웹사이트에서 A 웹사이트로 요청을 보낼 수 없기 때문이다.
즉, SOP는 브라우저의 기본적인 보안 정책으로서 그 의의가 있다고 할 수 있다.
그러나 기술의 발달과 함께 서로 다른 Origin끼리의 데이터 송수신이 필요한 경우가 많아졌고, 이로 인해 SOP에 별도의 예외 사항을 두게 되었다. RFC 6454에서 정의한 예외 사항은 다음과 같이 정리할 수 있다.
CORS는 '다른 origin으로 요청을 보내기 위해 지켜야 하는 정책'으로 정리할 수 있다. 원래대로라면 SOP에 의해 불가능한 요청(다른 origin으로의 요청)을 가능케 하기 위해 지켜야 할 정책인 것이다.
이때 한 가지 인지할 것은, CORS는 '브라우저'의 정책이라는 것이다. 즉, 서버는 평소처럼 응답을 하고, 브라우저가 자신이 보낸 요청과 서버로부터 받은 응답 데이터를 검사하여 CORS를 지키는지 확인하고, 안전한 요청을 보낸 것인지 확인하는 것이다.
CORS의 기본적인 동작 원리는 단순하다.
즉, CORS를 위해서는 서버에서 응답의 Access-Control-Allow-Origin 헤더에 허용되는 Origin의 목록만 설정해 주면 되는 것이다.
그러나 동작 원리를 조금 더 깊게 파고들면 시나리오에 따라 세 가지 유형으로 나눌 수 있다.
일반적으로 웹 어플리케이션 개발 시 가장 쉽게 마주하는 시나리오로서, 이 경우 브라우저는 요청을 한 번에 보내는 것이 아니라 예비 요청(Preflight request)과 본 요청으로 나누어 서버로 전송한다. 이 예비 요청에는 OPTION 메소드가 사용되는데, 브라우저는 이를 통해 본 요청을 보내기 전 이 요청을 보내는 것이 안전한지를 확인한다.
정식 명칭은 아니지만 MDN의 CORS 문서에서 Simple Request라고 부르는 시나리오이다. 이는 예비 요청 없이 본 요청을 서버에 보낸 후, 서버가 이에 대한 응답 헤더에 Access-Control-Allow-Origin 등의 값을 보내 주면 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다. 즉, 프리플라이트에서 예비 요청만 빠진 형태라고 이해하면 된다.
다만 단순 요청의 경우 특정 조건을 만족하는 경우에만 가능한데, 이 조건이 상당히 까다롭기 때문에 이 시나리오를 마주하는 경우는 드문 편이다.
- 요청의 메소드는 GET, HEAD, POST 중 하나이어야 한다.
- Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용해서는 안 된다.
- 만약 Content-Type을 사용하는 경우 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
3번째 시나리오는 인증된 요청을 사용하는 방법으로, CORS의 기본적인 방식이라기보단 다른 출처 간의 통신에서 보안을 강화하고 싶을 때 사용하는 방식이다.
기본적으로 브라우저의 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API 등은 별도 옵션 없이 브라우저의 쿠키 정보나 인증 관련 헤더를 함부로 요청에 담지 않는데, credentials 옵션을 통해 요청에 인증과 관련된 정보를 담을 수 있도록 해 준다.
이 옵션에는 총 3가지 값을 사용할 수 있으며, 각 내용은 다음과 같다.
옵션 | 내용 |
---|---|
same-origin(기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다. |
include | 모든 요청에 인증 정보를 담을 수 있다. |
omit | 모든 요청에 인증 정보를 담지 않는다. |