[Security] CORS
- -
CORS
CORS는 Cross-Origin Resource Sharing의 약자로, 교차 출처 리소스 공유를 말한다.
여기서 교차 출처(Cross-Origin)란 다른 출처를 의미한다.
CORS 정책은 교차 출처 리소스 공유에 대한 정책으로 해당 정책을 위반하게 되면 CORS 오류가 발생한다.
CORS 오류가 발생하게 되면 다음과 같은 오류 로그를 볼 수 있다.
🚨 Access to fetch at ‘https://www.domain.com/me’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
그럼 출처가 같은지 다른지 어떻게 판단하는 것일까?
출처(Origin)
먼저 URL 구조를 통해 출처에 대해서 알아보겠다.
해당 이미지는 URL 구조에 대한 이미지이다.
URL 구조에서 통신 프로토콜(http, https), 호스트, 도메인, 그림엔 나와있지 않지만 포트 번호까지를 합한 것을 출처로 본다.
SOP(Same-Origin Policy)
출처와 관련해서 SOP 정책이라는 것이 존재한다.
SOP는 Same-Origin Policy의 약자로, 같은 출처에서만 리소스를 공유할 수 있도록 하는 정책이다.https://www.domain.com
출처의 리소스를, 다른 출처에서는 사용할 수 없다는 것이다.
하지만 웹이라는 환경에서 다른 출처의 리소스를 사용할 수 없다는 것은 굉장히 치명적이다.
따라서 서로 다른 출처 간의 리소스 공유를 모두 막을 수는 없으니, SOP 정책에 몇가지 예외를 두고 SOP와 예외 조항들을 지킨 리소스 요청은 출처가 다르더라도 허용할 수 있게끔 한 것 중 하나가 CORS 정책을 지킨 요청이다.
예를 들어, A 출처에서 B 출처의 리소스를 요청한다면 출처가 다르기 때문에 SOP 정책을 위반한 것이 된다.
하지만 SOP의 예외 조항인 CORS 정책을 지켰다면, 해당 리소스를 사용할 수 있도록 허가한다.
SOP를 지키지 못했고 CORS 정책 또한 지키지 못했다면, A 출처는 B 출처의 리소스를 사용할 수 없다.
이러한 정책들이 생겨난 이유는 보안에 취약한 웹 환경의 특성 때문이다.
웹 환경은 모두에게 열려있는 오픈된 환경인데, 서로 다른 출처, 애플리케이션 통신 간에 아무런 제약이 없다면, 사이트, 애플리케이션에 여러 공격이 들어와 유저 정보가 탈취당하거나 서비스를 망가트리는 등의 문제가 생길 수 있다.
이러한 보안 문제를 막기 위하여 같은 출처에서만 리소스를 공유할 수 있도록 하는 SOP나 기타 여러 보안 정책들이 존재하는 것이다.
(참고)
SOP, CORS와 관련하여 출처를 비교하는 로직은 브라우저에 구현된 스펙이다.
CORS 정책을 위반하는 리소스 요청을 보내더라도 일반적으로 서버는 정상적으로 요청을 받고 처리하여 응답한다.
응답을 전달받은 브라우저는 이를 분석하여 CORS 정책 위반이라고 판단되면 해당 응답을 사용하지 않고 그냥 버리게 된다.
따라서 CORS 정책은 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버간에 통신할 때에는 적용되지 않는다.
또한 CORS 정책을 위반하는 요청 때문에 오류가 발생해도, 서버측에서는 정상적으로 응답을 했기 때문에 CORS 정책에 대해 재대로 이해하고 있지 않으면 트러블 슈팅에 어려움을 겪을 수 있다.
CORS 정책
CORS 정책이 SOP 정책의 예외 사항에 대한 정책이라는 것은 이제 알겠다. 그럼 CORS 정책을 지키려면 어떻게 해야할까?
클라이언트 요청부터 시작해서 서버가 요청을 처리하고 응답하여, 브라우저가 받는 것까지 차근차근 알아보자.
CORS 정책을 만족하지 않는 경우
먼저 CORS 정책을 만족하지 않는 경우를 살펴보겠다.
아래는 클라이언트가 HTTP 프로토콜을 통해 출처가 다른 서버에 리소스를 요청할 때의 흐름이다.
- (요청 생성) A 출처의 클라이언트가 B 출처의 서버에 리소스를 요청한다.
- (HTTP 프로토콜 전송) HTTP 프로토콜은 요청한 클라이언트의 출처 A를
Origin
헤더에 담아서 B 서버에 요청을 전송한다.Origin: A
- (서버 요청 처리 및 응답) 서버는 요청을 처리하고 응답을 반환한다.
- (브라우저) 브라우저에서 응답을 분석한다. 요청의
Origin
은 A이나 응답한 출처는 B이다. Same-Origin이 아니기 때문에 브라우저는 해당 응답을 사용하지 않고 버린다.
기본적으로 SOP 정책을 만족하지 않으면 서로 다른 출처간의 리소스 공유를 허가하지 않는다.
이제 CORS 정책을 만족하는 경우를 살펴보자.
CORS 정책을 만족하는 경우
CORS 정책을 만족하려면, 서버의 응답에 Access-Control-Allow-Origin
이라는 이름의 헤더에 요청을 허용할 출처를 추가해야 한다.
서버의 응답 헤더에 리소스를 요청한 출처가 추가되어 있으면, 브라우저는 해당 요청이 CORS 정책을 만족했다고 판단한다.
- (요청 생성) A 출처의 클라이언트가 B 출처의 서버에 리소스를 요청한다.
- (HTTP 프로토콜 전송) HTTP 프로토콜은 요청한 클라이언트의 출처 A를
Origin
헤더에 담아서 B 서버에 요청을 전송한다.Origin: A
- (서버 요청 처리 및 응답) 서버는 요청을 처리하고 응답을 반환한다.
- 이때, 서버 응답 헤더에
Access-Control-Allow-Origin: A
를 추가한다. - A 출처에서의 리소스 요청은 허용하겠다는 의미이다.
- 이때, 서버 응답 헤더에
- (브라우저) 브라우저에서 응답을 분석한다.
- 요청의
Origin
은 A이고 응답의Access-Control-Allow-Origin
도 A이다. CORS 정책을 만족한 것으로 판단한다.
- 요청의
CORS 정책의 기본적인 흐름은 위와 같이 요청의 Origin
헤더와 응답의 Access-Control-Allow-Origin
헤더를 비교하는 것으로 생각보다 간단하다.
하지만 세 가지 시나리오에 따라 CORS의 동작이 변경된다.
시나리오별 CORS 동작 방식
CORS 동작 방식은 세 가지의 시나리오에 따라 변경된다.
- Preflight Request
- Simple Request
- Credentialed Request
Preflight Request
Preflight Request 방식은 브라우저가 요청을 한번에 전송하지 않고 예비 요청과 본 요청으로 나누어 전송하게 된다.
이때, 브라우저가 본 요청 전송 전에 보내는 예비 요청을 Preflight라고 부르며, OPTIONS
HTTP 메서드를 사용한다.
이 예비 요청은 본 요청을 전송하기 전에 브라우저가 이 요청이 안전한 요청인지 확인하는 것을 목적으로 한다.
흐름을 나타내자면 다음과 같다.
- A 출처의 클라이언트가 B 출처의 서버로 요청을 전송하려고 한다.
- 브라우저는 요청의
Origin
헤더에 A 출처를 넣는다. - 요청의 목적지가 B 출처이기 때문에 SOP를 위반한 것을 확인, B 출처에 예비 요청(Preflight)을 전송한다.
- B 출처의 서버는 예비 요청에 대한 응답을 내린다.
- 브라우저가 예비 요청에 대한 B 출처의 응답을 확인한다. CORS 정책을 만족했다면 드디어 본 요청을 전송한다.
- B 출처의 서버는 본 요청을 받아 처리하고 결과를 응답한다.
- 브라우저는 해당 응답을 받아 작업을 처리한다.
예비 요청과 이에 대한 응답에는 어떤 값이 담기는지 확인해보자.
먼저 아래에는 예비 요청에 담기는 값들이다.
:authority: api.domain.com
:method: OPTIONS
:path: /users/me
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
access-control-request-headers: authorization
access-control-request-method: GET
cache-control: no-cache
origin: https://www.domain.com
pragma: no-cache
referer: https://www.domain.com/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
위 예비 요청을 확인해보면 요청을 보내는 Origin
정보와 더불어 본 요청에 전송될 추가 정보들도 함께 포함하여 전송하는 것을 알 수 있다.
다음으로 아래는 예비 요청에 대한 예비 응답이다.
access-control-allow-credentials: true
access-control-allow-headers: authorization
access-control-allow-methods: POST,GET,PUT,PATCH,OPTIONS,DELETE
access-control-allow-origin: https://www.domain.com
access-control-expose-headers: Authorization
content-length: 0
date: Sat, 12 Nov 2022 01:19:51 GMT
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
브라우저는 위 응답을 받아서 예비 요청의 Origin
헤더와 예비 응답의 Access-Control-Allow-Origin
헤더의 값을 비교한다.
추가로 Access-Control-Allow-*
헤더들의 값을 예비 요청의 값과 비교하여 CORS 정책 만족 여부를 판단한다.
위의 예비 요청과 예비 응답을 비교해보면 해당 요청은 CORS 정책을 만족하기 때문에 오류가 발생하지 않고 본 요청이 전송될 수 있다는 것을 알 수 있다.
주의!
여기서 주의할 점은 CORS 정책을 만족하지 않더라도 예비 요청에 대한 응답은 200 OK
로 표시된다는 점이다.
CORS 정책은 요청의 성공 여부와 관계없이 브라우저에서 판단한다.
예비 요청에 대한 응답은 성공적으로 받았기 때문에 200 OK
로 표시되고, 이후 브라우저에서 예비 요청과 예비 응답을 비교하여 CORS 정책 만족 여부를 확인한다.
이때, CORS 정책을 만족하지 못했다면 본 요청을 전송하지 않고 콘솔에 오류 로그를 출력한다.
"예비 요청에 대한 응답은 200 OK
인데 왜 CORS 오류가 발생했지?"라고 생각하지 말자.
중요한 것은 응답의 헤더에 올바른 값이 담겨있느냐 이다.
Simple Request
Simple Request 방식은 Preflight Request 방식과 흐름은 동일하다.
한가지 차이점은 Preflight Request 방식은 본 요청 전에 예비 요청을 보내는 반면, Simple Request 방식은 예비 요청을 보내지 않고 본 요청과 이에 대한 응답으로 CORS 정책 만족 여부를 판단한다.
Simple Request 방식을 언제든 사용할 수 있는 것은 아니다.
본 요청이 특정 조건을 만족할 때에만 예비 요청을 생략하고 Simple Request 방식을 사용할 수 있다.
- 본 요청의 HTTP Method가
GET
,POST
,HEAD
중 하나이다. 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
만 허용된다.
위 조건을 만족하지 않는다면 Simple Request 방식을 사용할 수 없다.
Credentialed Request
Credentialed Request 방식은 인증된 요청을 사용하는 방식으로 다른 출처간의 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방식이다.
기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest
, fetch
API는 별도의 옵션 없이 브라우저의 쿠키나 인증과 관련된 헤더를 함부로 요청에 담지 않는다.
이때 요청에 인증과 관련된 정보를 담게 해주는 옵션이 credentials
옵션이다.
해당 옵션은 3가지 값을 사용할 수 있다.
옵션값 | 설명 |
---|---|
same-origin(default) | 같은 출처 간 요청에만 인증 정보를 담는다. |
include | 모든 요청에 인증 정보를 담는다. |
omit | 모든 요청에 인증 정보를 담지 않는다. |
omit
의 경우, 요청에 인증 정보가 담기지 않기 때문에 Access-Control-Allow-Origin
헤더만을 확인하면 된다.same-origin
의 경우, 출처가 다른 요청에는 인증 정보가 담기지 않는다. 역시 Access-Control-Allow-Origin
헤더만을 확인하면 된다. 출처가 같은 요청은 SOP 정책을 지키게 된다.
하지만 include
의 경우, 출처가 다른 요청에도 인증 정보가 담길 수 있다.
이 옵션을 사용할 경우, 브라우저는 다른 출처의 리소스를 요청할 때, 단순히 Access-Control-Allow-Origin
헤더만을 확인하지 않고 추가적인 검사를 수행한다.
검사하는 내용은 다음과 같다.
Access-Control-Allow-Credentials
헤더에true
값이 반드시 존재해야 한다. 요청에 인증 정보를 담는 것을 허가한다는 의미이다.Access-Control-Allow-Origin
헤더는 모든 출처를 허용하는*
일 수 없다. 명시적으로 URL을 지정해줘야 한다.Access-Control-Allow-*
헤더 값들을 지정해야 하는 경우,*
를 사용할 수 없다. 명시적으로 지정되어야 한다.
위 조건을 만족하지 않는다면 CORS 정책을 위반했다는 브라우저 오류가 발생한다.
비동기 HTTP 요청을 보내기 위해 사용되는
XMLHttpRequest
,fetch
API에서는 요청에 인증 정보를 담기 위한 옵션 명이credentials
이다.
반면 axios API에서는 옵션 명이withCredentials
이다. 사용할 수 있는 값은true
,false(default)
이다.true
옵션은fetch
API의credentials: include
와 같이 모든 요청에 인증 정보를 담도록 허가한다.
반면false
옵션은credentials: same-origin
과 같이 같은 출처간의 요청에만 인증 정보를 담도록 한다.
'security' 카테고리의 다른 글
[Security] XSS와 CSRF 공격 (0) | 2022.12.10 |
---|
소중한 공감 감사합니다