CORS와 preflight, CORS와 관련된 헤더에 대해 알아본다.


Index

  1. Intro
  2. Cross-Origin-Resource-Sharing
  3. Preflight
  4. CORS 헤더
  5. 알쓸신잡

Intro

30분 투자로 CORS 전문가로 성장하기 세미나에서 사용했던 발표 자료를 정리해서 포스팅한다. 해당 포스트에서는 발표의 첫 꼭지 였던 CORS에 대해서 다룬다. CORS가 왜 발생하는지, CORS에서 함께 나오는 개념인 Preflight가 무엇인지 알아보고, 마지막으로 요청 헤더와 응답 헤더에 대해 알아본다.


[Picture 1] CORS Everywhere

MSA를 하다 보면 한 서비스에서 사용하는 도메인이 여러 개 일 수 밖에 없고 때문에 CORS 이슈도 자주 발생할 수 밖에 없다. 앞으로 CORS의 개념에 대해 알아보고 CORS에 대한 전반적인 이해도가 올라갔을 때 Cloudfront와 s3를 연결한 환경에서 CORS를 허용하는 설정 법을 알아보도록 하겠다. 마지막으로 캐시와 CORS가 만났을 때 발생할 수 있는 에러와 예방법 또한 알아보도록 하겠다.

이번 시간을 통해 CORS에 대해 잘 파악하고 CORS를 만나더라도 더 이상 겁먹지 않는 개발자로 거듭나도록 하자!


Cross-Origin-Resource-Sharing

Cross-Origin 이란 교차 출처를 뜻한다. 교차 출처란 서로 다른 도메인을 뜻한다. 같은 도메인에서 가져온 리소스들은 별도의 설정 없이 사용하는 것이 일반적이지만, 다른 도메인에서 가져온 리소스는 보안 문제가 발생할 수 있기 때문에 제한된다.

CORS는 이런 다른 도메인들끼리의 리소스 공유를 관리하기 위한 HTTP-header 베이스 메커니즘으로, 서로 도메인이 다르더라도 데이터를 안전하게 공유할 수 있도록 권한을 부여하거나 제한한다. CORS는 브라우저와 서버 사이의 secure한 corss-origin 요청을 서포트 하기 때문에 CORS를 위반했다는 에러는 브라우저를 사용할 때 볼 수 있다.


CORS는 어떤 리소스 요청을 보낼 때 사용할까?

CORS는 다른 도메인에서 가져온 리소스 접근을 제한한다. 처음에는 리소스라고 해서 단순 이미지, 오디오, 파일 등만 CORS 정책에 해당되는 줄 알았으나 텍스트 데이터, JSON 데이터 또한 CORS 정책에 해당된다. 즉, 미디어와 같은 정적 컨텐츠 뿐 아니라 API에서 만들어주는 동적 컨텐츠를 주고 받을 때도 CORS 정책이 적용된다.


CORS가 발생하는 경우

브라우저 → 서버 ✔

서버 → 서버 ❌

터미널 → 서버 ❌


leelee.im와 CORS가 일어나는 도메인들

  1. notleelee.im
  2. api.leelee.im
  3. leelee.im:3030
  4. http://leelee.im

[protocal]:[domain]:[port] 하나라도 다르면 다른 도메인으로 인식한다.


CORS 정책 위반

가장 간단한 CORS 접근 제어 프로토콜은 Origin 헤더와 Access-Control-Allow-Origin 을 사용하는 것이다. 요청 헤더에 Origin에 담겨있는 도메인이 응답 헤더의 Access-Control-Allow-Origin에 포함 되어 있다면 CORS 정책을 준수한 것이다.


예시 1) CORS 정책 준수

Request Origin: https://exchange.staging.kasa.sg

Response allow Origin: * (모든 도메인을 허용한다)

[Picture 2] CORS 허용 Response Header

이번에는 CORS 정책을 위반한 경우를 보겠다. 응답 헤더에 Access-Control-Allow-Origin이 포함이 안되어있다. 해당 헤더가 없을 경우 브라우저는 기본적으로 Same-Origin Policy에 따라서 동작한다. Same-Origin Poilcy는 다른 도메인의 리소스에 접근을 제한하는 메커니즘이므로, 리소스를 차단시켜버린다. 그 후 결과값으로 CORS Error를 반환한다.


예시 2) CORS 정책 위반

Request Origin: https://exchange.staging.kasa.sg

Response Allow Origin: ❌

[Picture 3] CORS 거부 Response Header

Preflight

다른 도메인에서 리소스를 요청하려면 브라우저는 몇 가지 보안 검사를 수행해야 하는데, 그 중 하나가 preflight 이다. preflight는 브라우저가 실제 요청을 보내기 전에 어떤 형식으로 요청을 보냈을 때 서버가 해당 요청을 허용하는지를 미리 확인한다.
브라우저는 OPTIONS 로 preflight를 보내고, 서버에게 (1) 어떤 헤더를 허용하는지 (2) 어떤 메소드를 허용하는 지 (3) 자격 증명을 함께 보내도 되는지 등의 서버 정책을 물어본다.

[Picture 4] Preflight flow

preflight를 사용하면 브라우저와 서버 간의 통신이 안전하게 예측 가능한 방식으로 이루어지게 된다. 서버는 요청을 허용하거나 거부하는 방식으로 CORS 정책을 관리하며, 브라우저는 이를 준수하여 보안 문제나 권한 없는 접근을 방지한다.

만약 preflight를 통해 실제 요청이 서버에서 허용하지 않는 형태라는 걸 알게되면 브라우저에서 실제 요청을 보내지 않고 에러 처리를 발생시킨다.


예시 3) exchange 사이트에서 preflight 요청

[Picture 5] OPTIONS Method

exchange 사이트를 보면 같은 URL로 OPTIONS 요청을 보내고 GET/PUT/POST 등의 요청을 보내는 것을 확인할 수 있다. 여기서 OPTIONS가 preflight 요청이다.


[Picture 6] Preflight Response

Preflight로 요청 헤더에서도, 응답 헤더에서도 CORS 관련 헤더를 설정한 것을 확인할 수 있다. 이 헤더들은 뒤에서 조금 더 자세히 다루도록 하고, 여기서는 preflight를 통해 CORS 정책을 브라우저와 서버가 주고 받았다는 걸 확인하고 넘어가겠다.


preflight을 보내지 않는 요청

preflight를 보내지 않는 요청도 있다. 서버에 Simple Request (간단한 요청)을 보낼 경우 preflight를 보내지 않고, 바로 실제 요청을 보낸다.

Simple Request의 조건

모두 충족할 때 만 Simple Request가 된다.

  • 메서드가 GET, HEAD, POST 중 하나이다.
  • 사용자 지정 헤더 없이, 브라우저가 자동 설정한 헤더만 가지고 있다.
    • 사용자 지정 헤더 예시: Authentication, X-something
  • Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나이거나 없다.

그럼 리소스를 가지고 있는 서버인 cloudfront에 접근할 때 preflight를 보내게 될까? 요청 헤더를 살펴보도록 하겠다.

[Picture 7] Request to cloudfront

(1) GET 메서드를 사용하고 (2) 사용자 지정 헤더가 없다. (3)Content-type도 명시되지 않았다. 모든 조건을 충족하여 해당 요청은 간단한 요청이 되어, preflight를 보내지 않는다. 하지만 요청 헤더에 Origin 값이 설정되어있고, Sec-Fetch-Mode가 cors로 되어있는 것을 보아 브라우저가 CORS 정책을 체크하고 있다는 것을 알 수 있다.

Sec-Fetch-Mode는 브라우저가 리소스를 가져오는 방식을 지정하기 위한 헤더 중 하나이다. 값이 cors로 되어있으면 CORS 정책을 준수하는 요청이다. 요청의 응답이 오면 응답 헤더 중 Access-Control-Allow-Origin과 같은 CORS 관련 헤더를 확인 후, 정책이 허용될 때만 브라우저에서 뿌려준다.

처음에는 aws에서 자체적으로 관리하고 있기 때문에 preflight를 보내도 브라우저에 preflight가 따로 안 나오는 줄 알았다. 그냥 안 보내는 것이었다. 😂


CORS 헤더

이번에는 요청 헤더와 응답 헤더에 사용하는 CORS 헤더를 알아보도록 하겠다. 공식 스팩인 Fetch Standard에 나와있는 헤더만을 기술했다.

요청 헤더

Origin

  • 요청을 보내는 서버 URL이다. 별도의 정보 값 없이 오직 서버 이름만을 포함한다.

Access-Control-Request-Method

  • preflight 요청을 날릴 때 사용한다.
  • 실제 요청에서 어떤 HTTP 메서드를 사용할지 서버에게 알려주기 위해 사용한다.

Access-Control-Request-Headers

  • preflight 요청을 날릴 때 사용한다.
  • 실제 요청에서 어떤 HTTP 헤더를 사용할지 서버에게 알려주기 위해 사용한다.

응답 헤더

Access-Control-Allow-Origin

  • 요청을 보낸 서버 URL인 Origin이 리소스에 접근 허용이 되었다는 걸 브라우저에 알려준다.
  • *의 경우 브라우저의 Origin에 상관없이 모든 리소스에 접근할 수 있다.
  • *가 아닌 도메인을 지정했다면 응답에 vary: Origin을 응답헤더에 포함해야 한다.
  • 클라이언트로 response header를 보낼 때 하나의 도메인만을 값으로 전달할 수 있다.
[Picture 8] access-control-allow-origin에는 하나의 도메인만!

Access-Control-Expose-Headers

  • 브라우저가 노출할 수 있는 헤더를 알려준다.
  • 브라우저는 CORS 요청에 대한 응답 헤더 중, 브라우저가 알아야 할 것만 노출하고 다른 것들은 보안을 위해 숨긴다. 하지만 Access-Control-Expose-Header 에 명시된 헤더는 브라우저가 노출을 할 수 있다.
1
Access-Control-Expose-Headers: X-Custom-Header

예를 들어 이렇게 응답이 왔을 경우, 브라우저가 X-Custom-Header 를 응답헤더에 노출한다. 응답헤더를 노출했을 경우 개발자 도구에서 확인할 수 있고, javascript나 다른 브라우저 API를 사용해서 이 헤더값을 읽고, 사용할 수 있게 된다.


Access-Control-Allow-Credentials

  • 브라우저가 요청을 보낼 때 인증 정보(쿠키, HTTP 인증 헤더 등)을 함께 보낼 수 있는지 알려준다.
  • 보안을 위해 default로 CORS 요청에서는 인증 정보를 함께 보내지 않는다.

브라우저는 인증 헤더 또는 쿠키에 사용자 인증 정보를 가지고 있는데, 브라우저의 입장에서 같은 도메인이 아닌 다른 도메인으로 요청을 보낼 때 사용자 인증 정보를 함께 보내는 것은 보안을 약화시키는 행위다. 그런데 서버에서 Access-Control-Allow-Credentialstrue로 보내면 브라우저는 서버가 사용자 인증 정보를 필요로 하며, 사용자 인증 정보를 안전하게 처리할 수 있을 것으로 간주하고 다음 요청에 사용자 인증 정보를 넣어 요청을 보낸다.


Access-Control-Max-Age

  • preflight 응답으로 사용된다.
  • preflight 요청 결과를 캐시할 수 있는 시간을 나타낸다.
  • (초) 단위다.

Access-Control-Allow-Methods

  • preflight 응답으로 사용된다.
  • 실제 요청을 날릴 때 사용할 수 있는 메서드를 나타낸다.

Access-Control-Allow-Headers

  • preflight 응답으로 사용된다.
  • 실제 요청을 날릴 때 사용할 수 있는 헤더를 나타낸다.

알쓸신잡

서버는 요청이 브라우저에서 온 걸 어떻게 알고 CORS 정책을 적용할까?

서버와 서버 사이에는 CORS 정책을 적용하지 않는다. 오직 서버와 브라우저 사이에서만 CORS 정책을 사용한다. 그럼 서버는 이 요청이 CORS 정책이 필요한 요청인지 아닌지를 어떻게 판단하고 응답 헤더에 CORS 관련 값을 넣어줄까?


[Picture 9] Origin, Sec-Fetch-Mode

위의 예제에서는 요청 헤더에 OriginSec-Fetch-Mode 를 보고, 서버에서 CORS 응답 헤더를 생성했다. 예제의 요청 헤더에는 없지만 Access-Control-Request-MethodAccess-Control-Request-Headers 또한 서버에게 CORS 관련 요청임을 알려주는 헤더이다.


브라우저가 아닌 터미널 또는 서버에서 요청을 보내보면 요청 헤더에 CORS와 관련된 Origin과 같은 헤더를 보내지 않는 것을 확인할 수 있다.

[Picture 10] curl 요청

(plus) curl 요청을 보낼 때 CORS 설정하기

1
curl -H "Origin:exchange.staging.kasa.sg" -v https://media.staging.kasa.sg/test.pdf

curl에서 origin을 설정해서 보내면 request에서도, response에서도 CORS 관련 헤더가 들어가있는 것을 확인할 수 있다.

[Picture 11] curl로 origin 보내기