CORS 너 뭐 돼?

·2022년 6월 15일
0

신기한 개발 세상

목록 보기
11/12

언제나 나를 화나게 하는 존재. CORS
웹 프로젝트를 하다보면 CORS 오류를 높은 확률로 만나볼 수 있다. 대체 CORS가 뭐길래 이렇게 자주 만나야하는 걸까?

CORS를 이해하려면 일단 SOP에 대한 이해가 필요하다.

SOP

SOP는 Same Origin Policy의 줄임말이다. 동일 출처(Same Origin) 에서만 자원(문서, 스크립트)들을 공유할 수 있도록하는 중요한 보안 정책이다. SOP를 통해 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여준다.

그럼 여기서 말하는 "동일 출처" 의 기준은 무엇일까?

동일 출처의 기준


위 사진처럼 URL에는 Protocal, Domain(Host), Port, Path 등등으로 이루어져 있다.

동일한 출처인지 비교하기 위해서는 Protocal, Domain(Host), Port 이 세가지가 모두 동일해야한다.

만약 API 서버의 주소가 https://www.example.com:8080이고 프론트 쪽 주소가 https://www.example.com:3000인 경우엔 Protocal과 Domain은 동일하지만 Port가 상이하므로 SOP의 부합하지 않기 때문에 별다른 설정을 하지 않았다면 CORS 이슈가 생기게된다.

그럼 CORS는 뭘까?

위에서 말했듯이 다른 Origin을 사용할 때 별다른 설정을 하지 않으면 CORS 오류가 난다고 했다. 이 별다른 설정 즉, 다른 Origin끼리도 자원을 주고 받을 수 있게 하는 것이 바로 CORS(Cross-Origin Resource Sharing)이다.

이전에는 동일한 도메인에서만 자료를 받았지만, 클라이언트와 서버의 도메인이 다른 경우가 빈번하게 발생하게되면서 더이상 동일한 도메인에서만 자료를 받아올 수 없게 됐다. 그러면서 CORS가 등장한 것이다.

CORS 동작원리

기본적으로 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.

이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-Allow-Origin이라는 값에 “이 리소스를 접근하는 것이 허용된 출처”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.

기본적인 흐름은 간단하지만, 사실 CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경된다. 시나리오에는 Preflight Request, Simple Request, Credentialed Request 가 있다. 각각에 대해 알아보자.

Preflight Request

Preflight 방식은 일반적으로 우리가 웹 어플리케이션을 개발할 때 가장 마주치는 시나리오이다. 이 시나리오에 해당하는 상황일 때 브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.

이때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 부르는 것이며, 이 예비 요청에는 HTTP 메소드 중 OPTIONS 메소드가 사용된다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.

  1. 우리가 자바스크립트의 fetch API를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내리면 브라우저는 서버에게 예비 요청을 먼저 보낸다.
  2. 서버는 이 예비 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보낸다.
  3. 브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책을 비교한다. 4. 요청을 보내는 것이 안전하다고 판단되면 같은 엔드포인트로 다시 본 요청을 보내게 된다.
  4. 서버가 이 본 요청에 대한 응답을 하면 브라우저는 최종적으로 이 응답 데이터를 자바스크립트에게 넘겨준다.

Simple Request

Simple Request은 예비 요청을 보내지 않고 바로 서버에게 본 요청부터 때려박은 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin과 같은 값을 보내주면 그때 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다. 즉, 프리플라이트와 단순 요청 시나리오는 전반적인 로직 자체는 같되, 예비 요청의 존재 유무만 다르다.

Simple Request를 보내기 위해선 아래 조건을 모두 만족해야한다.

  • GET, HEAD, POST 메소드만 사용 가능하다
  • User agent에서 자동으로 설정한 헤더 외 수동으로 설정할 수 있는 헤더는 Fetch에 정의된 헤더 뿐이다.
  • 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용한다.

Simple Request의 시나리오를 따르기 위해서는 사용자 인증을 위한 Authorization도 사용할 수 없으며, 대부분의 HTTP 요청에서 사용하는 Content-Type인 application/json도 설정할 수 없다. 이런 이유로 Simple Request보단 Preflight를 더 자주 마주하게 된다.

Credentialed Request

이 방식은 CORS의 기본적인 방식이라기 보다는 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법이다. 동일 출처인 경우엔 HTTP 통신을 하는 경우 별도의 설정 없이 쿠키가 요청 헤더에 들어간다. 하지만 CORS인 경우엔
기본적으로 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다.

이 옵션에는 총 3가지의 값을 사용할 수 있으며, 각 값들이 가지는 의미는 다음과 같다.

  • same-origin (기본값)
    같은 출처 간 요청에만 인증 정보를 담을 수 있다
  • include
    모든 요청에 인증 정보를 담을 수 있다
  • omit
    모든 요청에 인증 정보를 담지 않는다

만약 same-origin이나 include와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin만 확인하는 것이 아니라 좀 더 빡빡한 검사 조건을 추가하게 된다.

이처럼 요청에 인증 정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면 브라우저는 CORS 정책 위반 여부를 검사하는 룰에 다음 두 가지를 추가하게 된다.

  1. Access-Control-Allow-Origin: *를 사용할 수 없으며, 명시적인 URL이어야한다.
  2. 응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야한다.

인증까지 얶혀있는 이 시나리오는 다른 시나리오에 비해서 다소 복잡하게 느껴질 수는 있지만, 이렇게 CORS 정책에 대한 다양한 시나리오를 알아두면 실제 상황에서 CORS 정책 위반으로 인한 문제가 발생했을 경우 삽질해야하는 시간을 크게 단축시킬 수 있다.

CORS를 해결할 수 있는 방법

Access-Control-Allow-Origin 세팅하기
CORS 정책 위반으로 인한 문제를 해결하는 가장 대표적인 방법은, 그냥 정석대로 서버에서 Access-Control-Allow-Origin 헤더에 알맞은 값을 세팅해주는 것이다.

이때 와일드카드인 *을 사용하여 이 헤더를 세팅하게 되면 모든 출처에서 오는 요청을 받아먹겠다는 의미이므로 당장은 편할 수 있겠지만, 바꿔서 생각하면 정체도 모르는 이상한 출처에서 오는 요청까지 모두 받아드릴 수 있으므오 보안적으로 문제가 생길 수 있다.

그러니 가급적이면 귀찮더라도 Access-Control-Allow-Origin: 정확한 주소와 같이 출처를 명시하자.

Webpack Dev Server로 리버스 프록싱하기
CORS를 가장 많이 마주치는 환경은 바로 로컬에서 프론트엔드 어플리케이션을 개발하는 경우라고 해도 과언이 아니다. 백엔드에는 이미 Access-Control-Allow-Origin 헤더가 세팅되어있겠지만, 이 중요한 헤더에 http://localhost:3000 같은 범용적인 출처를 넣어주는 경우는 드물기 때문이다.

프론트엔드 개발자는 대부분 웹팩과 webpack-dev-server를 사용하여 자신의 머신에 개발 환경을 구축하게 되는데, 이 라이브러리가 제공하는 프록시 기능을 사용하면 아주 편하게 CORS 정책을 우회할 수 있다.

마무리

CORS 이슈는 누구나 한번씩은 겪는 문제이다. CORS가 생겼다고 당황하지말고 자신이 사용하고 있는 기술 명세를 상세하게 읽어보고 천천히 해결해나가면 된다. 케이스마다 해결법이 다르기 때문에 구글링해서 나오는 방법이 내가 찾는 답이 아닐 수 있음을 알면 좋겠다.

참고자료

https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
https://developer.mozilla.org/ko/docs/Learn/Common_questions/What_is_a_URL
https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/OPTIONS
https://developer.mozilla.org/ko/docs/Glossary/Preflight_request
https://evan-moon.github.io/2020/05/21/about-cors/#sopsame-origin-policy

profile
이제는 병아리는 벗어나야하는 프론트개발자

0개의 댓글