Operation/Network

CORS 총정리!! (+ Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true 해결)

JaeHoney 2022. 4. 22. 14:09

SOP (Same-Origin Policy)

SOP는 같은 출처에서만 리소스를 공유할 수 있다는 정책이다.

 

개발을 하다가 HTTP <-> HTTPS 간 통신에만 CORS 에러가 나는 경험이 많이 있었는데 그 이유가 SOP 정책 때문이다.

 

출처를 비교할 때는 URL의 구성요소 중 Protocol, Host, Port 3가지가 모두 동일해야 한다.

https://www.honey.com/로 요청을 보낸다고 가정하자.

  • https://www.honey.com/resources -> 가능
  • https://www.honey.com/resources?query=상품 -> 가능
  • http://www.honey.com/ -> 불가능 (프로토콜이 다르다)
  • https://www.bee.com/ -> 불가능 (호스트가 다르다)

 

그렇다면 SOP는 왜 필요할까?! 해커가 심은 악성 스크립트로 인해서 해커의 서버로 신용 정보를 전송하게 되면 큰일이다. 이러한 문제를 막기 위함이라고 보면 된다.

 

CORS(Cross-Origin Resource Sharing)

CORS는 교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)로 현재 출처가 아닌 다른 도메인에 요청을 보내는 것을 의미한다.

 

CORS 정책(CORS policy)은 SOP(Same-Origin Policy)로 인해 발생하는 문제를 해결해주기 위한 일종의 정책이다.

 

대표적인 에러로 다음과 같은 에러가 있다.

  • No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled
  • The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ’*’ when the request’s credentials mode is ‘include’.

 

CORS의 기본, Origin 요청 헤더

Origin이란 출처라는 뜻으로 요청을 보낸 곳의 주소(IP + Port)를 의미한다. 다른 출처로 요청을 보낼 때 Request Header에 Origin을 포함하게 된다.

 

 

 

CORS 동작 과정

1. 클라이언트에서 Request Header에 Origin을 담아서 전송한다.

2. 서버는 응답헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달한다.

3. 클라이언트에서 자신이 보냈던 요청의 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교한다.

(Access-Control-Allow-Origin이 *이라면 모두 허용한다.)

 

비교한 결과, 자신이 보낸 요청이 유효하지 않다면 그 응답을 사용하지 않고 버린다.

 

-> 어..? 이미 요청을 보내서 응답을 받은 것 아닌가?

 

아니다!

브라우저는 요청을 한번에 보내지 않고, 사전요청과 본요청으로 나누어 서버에 전달한다.
처음에 보낸 요청은 OPTIONS(HTTP METHOD)로 보낸 서버가 자신의 요청을 받아줄 수 있는지 확인하는 예비요청이다.

그래서 CORS 정책 에러가 발생하면 개발자 도구의 Network에 요청이 한 쌍으로 있다.

 

즉, 사전 요청에서 CORS 정책에 의한 제한으로 자신의 요청이 유효하지 않다고 판단하고 본 요청을 보내지 않는다.

 

-> 위 과정을 보면 CORS는 브라우저 내부적으로만 동작하는 것으로 보인다.

 

그렇다!

 

SOP 자체가 브라우저에서 사용자의 토큰이나 쿠키를 탈취하려는 시도를 방지하기 위해 제공하는 정책이다. 즉, 브라우저를 통하지 않고 요청을 보내거나 동일 출처 정책이 아니라면 요청을 보내고 응답을 받을 수 있다.

 

인증된 요청 (Credentialed Request)

다른 출처로 요청할 때 세션이나 쿠키 정보를 담아야 하는 일이 종종 발생한다.

 

메인에서 주문 API로 요청을 보낸다고 가정하자. 주문 API에서는 세션이나 쿠키가 없으면 사용자가 누구인지 알 수 없다.

 

요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 Credentials 이다.

  • (해당 Option이 false면 다른 출처로 요청 시 쿠키를 담아서 보내지 않는다.)
$.ajax({
    url: 'https://api.product.com',
    type: 'GET',
    beforeSend: function(xhr){
       xhr.withCredentials = true;
    }
});

 

문제는 인증된 요청을 사용할 때는 응답 헤더에 두 가지 Rule이 추가된다.

  • Access-Control-Allow-Credentials가 true일 것
  • Access-Control-Allow-Origin이 *이 아닐 것

 

첫 번째(Access-Control-Allow-Credentials: true)는 응답 헤더만 등록해주면 되니까 쉽게 해결할 수 있다.

두 번째(Access-Control-Allow-Origin: *) 때문에 자주 보는 아래의 에러를 많이 만나게 된다.

 

  • The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ’*’ when the request’s credentials mode is ‘include’.

 

아래에서 더 자세히 설명하겠다.

 

Access-Control-Allow-Origin

CORS 에러는 프로젝트를 새로 만들 때마다 겪는 문제다. 스트레스를 받지 않으려면 응답 헤더 세팅이 중요하다.

 

1. Access-Control-Allow-origin: *

res.setHeader('Access-Control-Allow-origin', '*');

모든 Origin에서 오는 요청을 전부 허용하므로 편해서 많이 사용하는 방법이다. 보안이 허술해진다는 단점이 있다.

 

2. Access-Control-Allow-origin: 특정 도메인

res.setHeader('Access-Control-Allow-origin', 'https://www.honey.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');

CORS 요청을 허용할 Origin을 명시하면 보안상 이점도 있고, 세션이나 쿠키도 전달받을 수 있다. 바람직하지만, 요청을 보낼 Origin이 여러 곳이라면 사용할 수 없다.

 

3. Access-Controller-Allow-origin: 요청 출처

res.setHeader('Access-Control-Allow-Origin', req.getHeader('Origin'));
res.setHeader('Access-Control-Allow-Credentials', 'true');

요청 헤더의 Origin을 그대로 Access-Control-Allow-Origin에 넣으면 정책상의 문제는 깔끔하게 해결된다. 다만 이런식의 해결법은 적절하지 않다. 정책을 무시하는 것 밖에는 되지 않는다.

 

4. Access-Controller-Allow-origin: 요청 출처 (허용 출처 목록에 포함되어 있는 경우에만)

<IfModule mod_headers.c>
    SetEnvIf Origin "http(s)?://(www\.)?(google.com|staging.google.com|development.google.com)$" AccessControlAllowOrigin=$0
    Header add Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
    Header merge Vary Origin
</IfModule>

해당 코드는 Apache의 .htaccess 파일의 일부이며, 요청 헤더의 Origin이 설정한 리스트에 포함된 경우에만 Access-Control-Allow-Origin을 설정해준다.

 

이 방법은 언급한 보안 문제나 정책 문제를 모두 해결할 수 있다. 물론 App단에서도 처리가 가능하다.

app.use((req, res, next) => {
    const corsWhitelist = [
        'https://domain1.example',
        'https://domain2.example',
        'https://domain3.example'
    ];
    if (corsWhitelist.indexOf(req.headers.origin) !== -1) {
        res.header('Access-Control-Allow-Origin', req.headers.origin);
        res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    }
    next();
});

이 방법이 가장 깔끔한 방법인 것 같다.

발생 상황

CORS 에러는 아래 중 어느 상황에서 발생할 수 있을까..?

  • 브라우저 <-> 서버
  • Native App (클라이언트) <-> 서버
  • 서버 <-> 서버

CORS 에러는 Native App, 서버에서 요청을 보낼 때는 발생하지 않는다. 앞에서 언급했듯 SOP는 브라우저에서 토큰이나 쿠키를 탈취하는 시도를 막기 위해 만든 정책이다.

 

즉, CORS의 요점은 한 도메인에서 다른 도메인으로 데이터를 수정하는 AJAX 혹은 HTTP 요청을 방지하기 위한 것이다.

네이티브 앱, 서버는 브라우저와 관계가 없고 특정 도메인에서 로드된 웹 페이지가 아니므로 CORS 제한이 필요하거나 적용되지 않는다! (PostMan도 이와 동일하다.)

 


 

Reference