CORS(작성중)
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?
URI
에 Path
이전 부분을 나타낸다.
Origin
은 페칭된 출처를 나타낸다. 쉽게 말하면 요청한 클라이언트가 어디서 요청했는지 그 위치를 알려준다.
브라우저의 개발자 도구에서 Location
객체가 가진 origin
의 프로퍼티에 접근하면 쉽게 그 값을 알아낼 수 있다.
console.log(location.origin);
VM76:1 https://lkic1625.github.io
SOP(Same-Origin Policy)
SOP
는 RFC6454
에서 처음 등장한 보안 정책으로 같은 출처에서만 리소스를 공유할 수 있다는 정책이다.
하지만 웹 환경에서는 다른 출처의 리소스를 가져오는 일이 굉장히 흔하므로 예외 조항인 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
)를 사용하여 권한을 처리한다.
단순 요청은 매우 간편해 보이지만, 보안적으로 상당히 취약하므로 특정 상황에서만 가능하다.
- 요청의 메소드는 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만 허용된다.
제약 조건을 알아봤으니 이제 직접 예제를 봐보자. 예를들어, 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 cookies
와 HTTP 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
가 가지고 있는 withCredentials
을 true
로 변경했다.
이 프로퍼티는 기본적으로 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/