CORS(작성중)

5 분 소요

Introduction

node 개발할 때, 따로 CORS란 단어를 책에선 본 적은 있지만 제대로 다루지는 않았다 헤더에 와일드 카드를 추가하던가, cors 모듈을 사용하여 가능한 URI를 설정하라는 것은 익히 알고 있었으나 그 속 내부까지 파고들어 가볼까 한다.

CORS란

이미지

Cross Origin Resource Sharing(CORS)는 웹서버를 개발하다보면 자주 마주칠 수 있는 상황이다.

이는 도메인 또는 포트가 다른 서버의 자원을 요청할 경우 보안상의 이유로 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한한다.

API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.

간단히 요약하자면 브라우저에서 서버로 하는 요청은 반드시 같은 Origin이어야 한다는 것이다.

Overview

CORS 표준은 웹 브라우저에서 해당 정보를 읽는 것이 허용된 Origin을 서버에서 설명할 수 있도록 새로운 HTTP 헤더를 추가한다.

보통은 preflight를 우선 OPTIONS 메서드로 전달 후 서버에서 허가가 떨어졌을 경우 실제 요청을 보내도록 진행된다 . 필요한 경우 Authenticate를 요청할 수 있다.

CORS를 사용하는 요청?

CORS 표준은 다음과 같은 경우 HTTP 요청을 허용한다.

  • Fetch API 호출
  • 웹 폰트
  • WebGL 텍스쳐
  • drawImage()
  • 이미지로부터 추출하는 CSS Shapes

Origin?

URIPath 이전 부분을 나타낸다.

Origin은 페칭된 출처를 나타낸다. 쉽게 말하면 요청한 클라이언트가 어디서 요청했는지 그 위치를 알려준다.

브라우저의 개발자 도구에서 Location 객체가 가진 origin의 프로퍼티에 접근하면 쉽게 그 값을 알아낼 수 있다.

console.log(location.origin);
VM76:1 https://lkic1625.github.io

SOP(Same-Origin Policy)

SOPRFC6454에서 처음 등장한 보안 정책으로 같은 출처에서만 리소스를 공유할 수 있다는 정책이다.

하지만 웹 환경에서는 다른 출처의 리소스를 가져오는 일이 굉장히 흔하므로 예외 조항인 CORS 정책에 한해 출처가 다른 리소스를 허용했다.

Access to network resources varies depending on whether the resources are in the same origin as the content attempting to access them.

Generally, reading information from another origin is forbidden. However, an origin is permitted to use some kinds of resources retrieved from other origins. For example, an origin is permitted to execute script, render images, and apply style sheets from any origin. Likewise, an origin can display content from another origin, such as an HTML document in an HTML frame. Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing.

Same Origin?

Origin에서 가장 잘 알아둬야할 점은 같은 Origin을 구별하는 사항이 서버의 구현된 스펙이 아닌 브라우저에 구현된 스펙이라는 것이다. 브라우저마다 IE 같은 경우는 포트가 달라도 호스트가 같다면 같은 출처로 보며, 포트마다 다른 출처라고 하는 브라우저도 존재한다.

서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답을 하고, 브라우저에서는 CORS 정책 위반이라고 판단하고 응답을 폐기할 수 있다는 말이다.

서버는 계속해서 정상적으로 응답한다고 뜨는데, 계속해서 오류가 나는 경우 디버깅이 매우 힘들 것이다. 따라서, CORS 관련하여 제대로 알고 있는 것이 매우 중요하다.

접근 제어 시나리오

Simple requests

단순 요청은 서버간에 간단한 통신을 하고, 교차출처 전용 헤더(Access-Control-Allow-Origin)를 사용하여 권한을 처리한다.

단순 요청은 매우 간편해 보이지만, 보안적으로 상당히 취약하므로 특정 상황에서만 가능하다.

  1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
  2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
  3. 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.

제약 조건을 알아봤으니 이제 직접 예제를 봐보자. 예를들어, https://foo.example 의 웹 컨텐츠가 https://bar.other 도메인의 컨텐츠를 호출하길 원할 때, foo.example에 배포된 자바스크립트에는 아래와 같은 코드가 사용될 수 있다.

const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

이미지

Request
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example

Response
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]

요청 헤더에서 Origin: https://foo.example과 응답 헤더의 Access-Control-Allow-Origin: *Simple request의 핵심이며, 와일드카드(*)를 사용할 경우 모든 경우의 교차출처를 허용하겠다는 의미다.

Preflight request

프리플라이트 요청은 앞에서 말한 단순 요청과 달리 먼저 OPTIONS 메서드를 통해 다른 도메인의 리소스 HTTP 요청이 가능한지 먼저 확인한다.

요청시 유저에게 영향을 줄 수 있기 때문에 사전 전송을 통해 안전한지를 확인하는 작업이다.

이미지

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

OPTIONS 메서드를 사용해 프리플라이트 요청을 날린 결과물이다. SAFE METHOD이기에 서버의 resource를 변경하지 않는다.

프리플라이트 요청에서 주의깊게 살펴볼 부분은 아래와 같다.

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
  • Access-Control-Request-Method: POST: 실제 요청에서 전송할 메서드를 알려준다.
  • Access-Control-Request-Headers: X-PINGOTHER, Content-Type: 실제 요청에서 이 헤더를 함께 전송한다는 의미다.

이번엔 응답 부분을 살펴보자.

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 8640
  • Access-Control-Allow-Origin: http://foo.example: 요청 가능한 출처를 의미합니다.
  • Access-Control-Allow-Methods: POST, GET, OPTIONS: 요청 가능한 메서드를 의미한다.
  • Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  • Access-Control-Max-Age: 8640:

아래는 이후 직접적인 교차 출처 요청, 응답 헤더이다.

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache

<person><name>Arun</name></person>


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

Credentialed request

인증정보를 포함한 요청은 HTTP cookiesHTTP Authentication 정보를 인식 가능하다.

요청 시 XMLHttpRequest, fetch에 함부로 쿠키 정보나 인증과 관련된 헤더를 담을 수 없다. 이를 지원하게 해주는 것이 credentials 옵션이다.

예제를 보며 이야기하자.

const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';

function callOtherDomain() {
  if (invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send();
  }
}

7번째 줄을 확인해보면, XMLHttpRequest가 가지고 있는 withCredentialstrue로 변경했다. 이 프로퍼티는 기본적으로 false값을 가지는 boolean 타입이다.

실제로, Access-Control-Allow-Credentials: true을 포함하지 않을 경우 브라우저는 현재 요청이 단순 GET요청이므로 기각할 것이다.

이미지

GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain


[text/plain payload]
Refernce
  • https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
  • https://evan-moon.github.io/2020/05/21/about-cors/