교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다. - MDN
여기서 origin(출처) 이란 scheme(protocol), host(domain), port 로 구성
예를들어 https://www.google.com/maps 라는 주소가 존재한다면
protocol: https://
Host: www.google.com
Port: 443
동일 출처(Same Origin)란 protocol, host, port 가 모두 같을때를 말한다.
👉 다른 출처의 어플리케이션이 서로 통신하는 것에 대해 아무런 제약도 존재하지 않는다면 악의를 가진 사용자가 소스 코드를 보고 CSRF(Cross-Site Request Forgery) 나 XSS(Cross-Site Scripting) 와 같은 방법을 사용하여 정보를 탈취할 수 있다.
👉 브라우저에서 다른 서버로 리소스를 요청할 경우 SOP를 우회하기 위한 여러가지 방법 중 가장 권장되는 방법
CORS 요청이 온다면, 서버는 Access-Control-* 헤더를 메시지에 포함시킬 수 있습니다.
Cache-Control
Content-Language
Content-Length
Content-Type
Expires
Last-Modified
Pragma
Access-Control-Allow-Credentials : 추후 설명
서버는 위와 같은 헤더를 통해 리소스에 대한 CORS 요청을 어느 수준까지 허용할 것인지 클라이언트(브라우저)에게 알려주고, 이를 바탕으로 브라우저는 추후동작을 결정합니다.
다음과 같은 조건을 만족하면, 브라우저는 CORS 요청을 Simple request로 처리합니다.
GET
POST
HEAD
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
이 방식은 일반 요청이랑 동일하게 동작하며, 단 한가지 다른 점은 응답의 Access-Control-Allow-Origin
에 따라 브라우저가 응답을 정상적으로 처리할지를 결정합니다.
예를 들어 http://localhost:8080
에서 서버(http://localhost:9000/api/menus)
에 요청을 보낸다면, 다음과 같은 요청 메시지가 전송됩니다. 간단하게 Origin 헤더만 추가해서 보내줍니다.
GET /api/menus HTTP/1.1
Host: localhost:9000
Referer: http://localhost:8080/
Origin: http://localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
...
아래 응답에 Access-Control-Allow-Origin
헤더가 존재하지 않기 때문에 브라우저는 이 응답을 자바스크립트 코드에 노출시키지 않고 오류를 뱉어냅니다.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 155
...
교차 출처 요청 차단: 동일 출처 정책으로 인해 http://localhost:9000/api/menus에 있는 원격 자원을 차단하였습니다. (원인: ‘Access-Control-Allow-Origin’ CORS 헤더가 없음).
서버에 Access-Control-Allow-Origin 헤더를 추가
해서 응답하면 다음과 같이 정상적으로 자바스크립트 코드에서 응답을 처리할 수 있습니다.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8080
Content-Type: application/json; charset=utf-8
Content-Length: 155
...
Simple request가 아니면 브라우저는 실제 요청을 보내기 전에 한 가지 작업을 수행합니다. preflight request를 실제 요청 메시지보다 서버에 먼저 보냅니다. 서버는 이에 대한 응답을 주고, 브라우저는 이를 통해 실제 요청을 보낼지 결정합니다.
Cross-origin 요청은 유저 데이터에 영향을 줄 수 있기 때문에 Preflight 요청을 한다.
OPTIONS 요청과 함께 두 개의 다른 요청 헤더가 전송된다.
아래에서 첫 행은 실제 요청을 전송할 때 POST 메서드로 전송된다는 것이고, 두번째 행은 실제 요청을 전송 할 때 X-PINGOTHER 와 Content-Type 사용자 정의 헤더와 함께 전송된다는 것을 서버에 알려준다.
Access-Control-Request-Method: POST // 실제요청의 메서드
Access-Control-Request-Headers: X-PINGOTHER, Content-Type // 실제요청의 추가헤더
OPTIONS /api/menus HTTP/1.1
Host: localhost:9000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Referer: http://localhost:8080/
Origin: http://localhost:8080
...
서버가 메서드와 헤더를 받을 수 있음을 알려준다. 마지막행은 preflight request에 대한 응답을 캐시할 수 있는 시간(초)이다. 아래에서는 86400초 (=24시간)
프리플라이트를 보내면 사전, 실제 요청 두번이 매번 왔다갔다 하므로 브라우저가 캐싱을 해두고 똑같은 요청을 보낼때, 사전 요청을 보내지 않고 바로 본 요청을 보낸다.
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: 86400 // Prefilght 응답 캐시기간
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: -1
Connection: keep-alive
Content-Length: 0
preflight request가 완료되면 실제 요청을 전송
응답 성공
GET /api/menus HTTP/1.1
Host: localhost:9000
Referer: http://localhost:8080/
Content-Type: application/json
Origin: http://localhost:8080
...
응답 실패
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Max-Age: -1
Connection: keep-alive
Content-Length: 0
실패 시 메세지
교차 출처 요청 차단: 동일 출처 정책으로 인해 http://localhost:9000/api/menus에 있는 원격 자원을 차단하였습니다. (원인: CORS 사전 점검 응답의 헤더 ‘Access-Control-Allow-Headers’에 따라 헤더 ‘content-type’가 허용되지 않음)
교차 출처 요청 차단: 동일 출처 정책으로 인해 http://localhost:9000/api/menus에 있는 원격 자원을 차단하였습니다. (원인: CORS 요청이 성공하지 못함)
Access-Control-Allow-Credentials
CORS는 기본적으로 보안상의 이유로 쿠키를 요청으로 보낼 수 없도록 막고 있습니다.
하지만 다른 도메인을 가진 API 서버에 자신을 인증해야 정상적인 응답을 받을 수 있는 상황에서는 쿠키를 통한 인증이 필요합니다.
📣 브라우저 요청 시 설정 옵션
✓ omit : 절대로 cookie 들을 전송하거나 받지 않는다.
✓ same-origin : 동일 출처(same origin)이라면, user credentials (cookies, basic http auth 등..)을 전송한다. (default 값)
✓ include : cross-origin 호출이라 할지라도 언제나 user credentials (cookies, basic http auth 등..)을 전송한다.
예시)
fetch('주소', {
credentials: 'include', // 모든 요청에 인증 정보 포함
});
// axios 로 통신할 시, withCredentials 설정을 true 로 넣어주면 된다.
axios.post(주소, 데이터, { withCredentials: true });
// 또는 공통으로 추가
axios.defaults.withCredentials = true;
또한 credentials 설정을 include/true 로 설정하면 CORS정책에 의해 Access-Control-Allow-Origin을 모든 출처를 허용하는 '' 로 지정할 수 없다는 에러가 발생하며, 따라서 cors 설정에서 을 입력하여 모든 출처를 허용한 경우에는 특정 출처를 정확히 명시해야 한다.
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
Access-Control-Allow-Credentials: true
그래서 여러 출처의 접근 허용을 위해 값을 쓰는 경우가 많습니다. 하지만 는 모든 출처의 접근을 허용하는 것이기 때문에 보안상 좋지 않습니다. 그렇다면 여러 출처를 허용하고 싶다면, 어떤 방식을 취하는 것이 좋을까요?
요청이 오면 CORS 요청 헤더의 Origin 값을 확인하여, 별도로 관리하고 있는 Whitelist에 포함되고 있는지 확인하면 됩니다.
만약 존재한다면, Access-Control-Allow-Origin헤더의 값으로 해당 Origin의 값을 넣어 응답할 것이고, 브라우저는 실제 요청을 서버에 보낼 것입니다. 악의적인 사이트에서 요청을 보냈더라도, Whitelist에 포함되어 있지 않기 때문에 안전하게 CORS 요청을 처리할 수 있습니다.
CORS의 Whitelist를 직접 처리할 수도 있지만, cors 미들웨어가 이미 지원하고 있기 때문에 다음과 같이 작성하면 Whitelist를 편하게 관리할 수 있습니다.
// express cors
// https://www.npmjs.com/package/cors#configuration-options
app.use(
cors({
origin: ["http://localhost:8080", "http://localhost:7070"],
})
);
Access-Control-Allow-Origin 헤더 값을 설정할 때, 자주 하는 실수 중 하나가 host 이름만 명시하는 경우입니다. www.test.com과 같이 scheme(protocol)를 생략하거나 포트번호가 80번이 아닌데도 포트번호를 생략한 경우입니다.
Accecc-Control-Allow-Origin의 헤더 값은 origin 스펙을 만족하는 값이기 때문에 [scheme]://[host]:[port] 형태로 명시해야 합니다. origin 스펙을 준수하지 않으면, 브라우저 단에서 Origin 헤더의 값과 비교를 잘못할 수 있기 때문에 꼭 origin 스펙을 준수해야 합니다.
CORS는 응답이 Access-Control-Allow-Credentials: true 을 가질 경우, Access-Controll-Allow-Origin의 값으로 *를 사용하지 못하게 막고 있습니다.
Access-Control-Allow-Credentials: true를 사용하는 경우는 사용자 인증이 필요한 리소스 접근이 필요한 경우인데, 만약 Access-Control-Allow-Origin: *를 허용한다면, CSRF 공격에 매우 취약해져 악의적인 사용자가 인증이 필요한 리소스를 마음대로 접근할 수 있습니다. 그렇기 때문에 CORS 정책에서 아예 동작하지 않도록 막아버린 것입니다.
Access-Contorl-Allow-Credentials: true인 경우에는 반드시 Access-Control-Allow-Origin의 값이 하나의 origin 값으로 명시되어 있어야 정상적으로 동작합니다.
출처