여러 프로젝트를 진행하면서 정말 수많은 CORS 오류를 경험해보았다. 그럴때마다 시간이 급하다보니 간단한 해결방법만을 서치해서 적용한 뒤 바로 넘어갈 때가 많았는데, 이번에 CORS에 대해 제대로 알아보고 싶어 이렇게 정리를 해보고자 한다.
이를 제대로 이해하기 위해서는 출처의 개념과 동일 출처 정책에 대해 인지하고 있어야한다.
📙출처?
URL의 스킴(프로토콜), 호스트(도메인), 포트로 정의되며, 보통 두 객체의 스킴, 호스트, 포트가 모두 일치하는 경우 같은 출처를 가졌다고 말한다.
예를 들어보자!
http://example.com/app1/index.html 과 http://example.com/app2/index.html 스킴(http)과 호스트(example.com) 일치하기 때문에 동일한 출처(origin)이다.
http://Example.com:80 과 http://example.com HTTP의 기본 포트는 80이므로 동일한 출처(origin)이다.
http://example.com/app1 과 https://example.com/app2 다른 스킴(http, https)을 가지고 있기 때문에 다른 출처(origin)이다.
http://example.com 과 http://www.example.com 과 http://myapp.example.com 다른 호스트를 가지고 있기 때문에 다른 출처(origin)이다.
http://example.com 과 http://example.com:8080 다른 포트(80, 8080)를 가지고 있기 때문에 다른 출처(origin)이다.
📍동일 출처 정책(same-origin policy)
어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식이다.
동일 출처 정책은, 서로 다른 출처 사이에서의 상호작용을 통제한다. 여기서 발생할 수 있는 상호작용은 크게 세가지로 나뉜다.
1. Cross-origin writes (교차 출처 쓰기)
일반적으로 허용하며, Cross-origin 으로의 링크, 리다이렉트, form submit이 있다.
2. Cross-origin embedding (교차 출처 삽입)
일반적으로 허용하며, 아래의 예시들이 있다.
<script src="..."></script>
로 추가하는 JavaScript.<link rel="stylesheet" href="...">
로 적용하는 CSS.<img>
로 표시하는 이미지.<video>
와<audio>
로 재생하는 미디어.<object>
와<embed>
로 삽입하는 외부 리소스.@font-face
로 적용하는 글씨체.<iframe>
으로 삽입하는 모든 것.3. Cross-origin reads (교차 출처 읽기)
일반적으로 불허한다. 그렇지만 종종 교차 출처 삽입 과정에서 읽기 권한이 누출된다.
나는 이 부분이 조금 이해하기 어려웠는데,
1
2
1. 단순 페이지 이동을 왜 "교차 출처 쓰기"라고 말하는 거지?
2. "교차 출처 읽기"가 왜 안전하지 않은거지? 일반적으로 쓰는게 더 위험한거 아닐까?
라는 생각 때문이었다.
1번은 결국 Cross-origin의 페이지를 받아와서 브라우저에 쓰는거니까, “쓰기”라고 표현하지 않았을까? 라고 어림짐작해보고 (만약 틀렸다면 알려주세요…)
2번은 해당 블로그에 정말 잘 설명되어 있다… Same-origin policy, CORS
요약하자면 교차 출처 읽기는 스크립트 내에서, Cross-origin의 사이트가, 페이지 이동 없이, 다른 사이트의 정보를 읽어오는 것을 금지한다는 이야기.
이러한 시나리오를 예상하면 더 이해하기 쉽다.
- 유저 A가 은행 업무를 보기위해 은행 웹 페이지에 로그인을 했다.
- 이후 로그아웃을 하지 않은 상태로, 유저 A의 은행 정보를 노리는 악성코드가 있는 페이지에 접속하였다. 2-1. 악성코드를 가진 페이지가 은행 페이지로의 form submit을 통해 교차 출처 쓰기를 시도한다. -> 어차피 은행 페이지로 리다이렉트되기 때문에 위험한 상황이 발생하지 않음 (=악성 페이지가 가져가는 정보 없음) 2-2. 악성코드를 가진 페이지가 교차 출처 읽기를 시도한다. -> 원래는 은행 사이트에만 전달되어야 하는 쿠키나 세션이 악성코드를 가진 페이지에게 전달된다. 이를 통해 유저 A 의 은행 정보를 가져올 수 있다.
이러한 보안상의 이유로, 브라우저에서는 스크립트에서 시작한 교차출처 HTTP 요청(=교차 출처 읽기 요청)을 제한한다.
즉, http://example1.com 라는 origin에서, 스크립트로 http://example2.com/api 로 요청을 보낼때, 아예 http://example2.com/api 에 닿기도 전에, 브라우저 측에서 제한한다는 의미다.
즉, 다른 출처로 요청을 보내게 되면 동일 출처 정책 때문에 브라우저에서 제한을 걸게 되고, 이를 해결하기 위해 CORS를 이용해 권한을 부여하는 것이다.
그리고 XMLHttpRequest
와 Fetch API
는 이러한 동일 출처 정책(same-origin policy)를 따른다. (그러면 axios는 CORS를 신경쓰지 않아도 되는걸까…? 궁금하다.)
🔗CORS란?
이제 CORS가 무엇인지 대충 감이 올수 있을거라 생각한다.
Cross-Origin Resource Sharing (CORS). 직역하면 교차 출처 리소스 공유. 한 출처(origin) 에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제
그렇다면 어떠한 방식으로 다른 출처에 접근할 수 있게 만들어 주는걸까?
✉️프리플라이트(preflight)
직역하자면 사전 요청. 본격적인 교차 출처 HTTP 요청 전에 서버 측에서 그 요청의 메서드와 헤더에 대해 인식하고 있는지를 체크하는 것이다.
만약 https://foo.example (클라이언트) 가 https://bar.other/doc (서버) 에 요청을 날린다고 가정해보자!
이렇게 클라이언트가 OPTIONS 메서드를 통해 서버로 HTTP 요청을 보내 실제 요청을 전송하기에 안전한지 확인한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// https://foo.example 쪽 (클라이언트)
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
// https://bar.other/doc 쪽 (서버)
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
클라이언트 측의 Access-Control-Request-Method
는, ‘이따가 이 메소드 요청할거야’ 라는 의미고 Access-Control-Request-Headers
는, ‘이따가 이 헤더도 같이 전송할거야’ 라는 의미다.
이러한 사전 요청은 일반적인 상황에서는 브라우저에서 자동으로 발생된다. 그러므로 프론트엔드 개발자가 이 요청을 직접 작성할 필요는 없다.
단, 서버는 위의 요청을 수락하기 위해서 Access-Control-Allow-Origin: https://foo.example
를 통해 클라이언트의 origin을 미리 등록해두어야 한다.
이제 이러한 에러가 이해가 된다!
XMLHttpRequest
요청을 하기 위해 클라이언트 측에서 프리플라이트를 날렸지만, 서버에서 Access-Control-Origin
에 해당 클라이언트의 origin을 등록해두지 않았기 때문에, 요청이 거절된 것이다.
📃단순 요청 (Simple Request)
일부 요청은 이러한 preflight를 요청하지 않는다.
이는(=단순 요청) 웹 컨텐츠가 이미 발행할 수 있는 것과 동일한 종류의 cross-site 요청입니다. 서버가 적절한 헤더를 전송하지 않으면 요청자에게 응답 데이터가 공개되지 않습니다. 따라서 cross-site 요청 위조를 방지하는 사이트는 HTTP 접근 제어를 두려워 할 만한 부분이 없습니다
즉, 서버가 무언가를 건들이지 않는 이상 요청자에게 정보가 갈 일이 없기 때문에, 보안상의 위험성이 없다고 생각하여 preflight 없이 요청할 수 있게 한 것이다.
이러한 요청은 아래와 같다.
- 다음 중 하나의 메서드
- GET
- HEAD
- POST
- 유저 에이전트가 자동으로 설정 한 헤더
- Accept
- Accept-Language
- Content-Language
- Content-Type (아래의 추가 요구 사항에 유의)
- Content-Type 헤더는 다음의 값들만 허용.
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 요청에 사용된 XMLHttpRequestUpload (en-US) 객체에는 이벤트 리스너가 등록되어 있지 않는다.
- 요청에 ReadableStream 객체가 사용되지 않는다.
🔐인증 정보를 포함한 요청(Requests with credentials)
해당 요청은 HTTP cookies
와 HTTP Authentication
정보를 인식한다. 그러나 기본적으로 cross-site XMLHttpRequest 나 Fetch 호출에서 브라우저는 자격 증명을 보내지 않는다. 그렇기 때문에 XMLHttpRequest 객체나 Request 생성자가 호출될 때 특정 플래그를 설정해야 한다.
1
2
3
4
5
6
7
8
9
10
11
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();
}
}
특정 플래그는 프론트엔드 쪽에서 이러한 방식으로 설정할 수 있다.
주요하게 봐야할 것은 withCredentials = true
그렇지만 이런다고 끝이 아니다.
서버측에서 Access-Control-Allow-Credentials
헤더를 추가해줌으로써 credentials 플래그가 true일 때 요청에 대한 응답을 표시할 수 있는지를 설정해주어야한다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 클라이언트
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
클라이언트는 Cookie
를 통해 쿠키를 담아 보냈고 서버는 쿠키를 받아오기 위해 Access-Control-Allow-Credentials
를 true
로 주었다.
브라우저는 Access-Control-Allow-Credentials: true
헤더가 없는 응답을 거부하기 때문에, 만약 서버에서 위의 속성을 설정하지 않는다면 서버의 응답을 받아올 수 없다!!
다음은 내가 겪은 다양한 CORS 에러 사례와, 해결 방법을 써보고 싶다…
참고
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/OPTIONS https://developer.mozilla.org/ko/docs/Glossary/Preflight_request https://fetch.spec.whatwg.org/#http-cors-protocol https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy https://developer.mozilla.org/ko/docs/Glossary/Origin https://en.wikipedia.org/wiki/Same-origin_policy