CORS Policy

박세영·2022년 3월 23일
0
post-thumbnail

이번에 CORS 에러를 핸들링 하면서 공부하게 된 내용을 정리해보려 한다.(정작 그 문제는 CORS와 연관이 없었다😂)

개발자가 보통 CORS 에러를 겪는 상황은 프론트앤드애서 백앤드로 요청을 보낼 때이다. 처음에는 Host와 port만 맞춰주면 되는 줄 알았는데 아니였다. CORS 에러를 발생시키는 원인에 대해서 차근차근 알아보자.

CORS에 대한 기본적인 내용

CORS(Cross-Origin Resource Shareing)는 웹에서 다른(Cross) 출처(Origin)로의 자원(Resource) 요청(Sharing)을 제한하는 정책이다. 그러니까 다른 Origin에서 자원에 대한 요청이 발생할 때 특정 조건들을 지켜야 한다는 의미이다.

CORS 정책이 존재하는 이유는 다른 Origin 받아오는 리소스들이 안전하다는 최소한의 안전장치를 제공하기 위해서이다. 여기서 출처 혹은 Origin은 정확이 무엇을 의미하는 것일까?

Origin

서버의 경로를 의미하는 URL안에는 다양항 정보가 담겨져 있다.

이 중 Protocol, Host 그리고 그림에는 생략되어 있는 port번호(리액트의 경우 :3000) 모두가 Origin에 해당한다. 이 3가지 구성 요소중 하나라도 다른 URL에 요청을 날린다면 Cross Origin Reques가 되는 것이다.

요청에 대한 출처를 비교하는 로직은 의외로 브라우저에서 구현되어 있는 스펙이다. 브라우저는 요청이 CORS 정책에 부합하는지를 검사하고 만약 CORS 정책 위반이라면 해당 요청은 이루어지지 않는다.

CORS는 대체 왜 있는 걸까?

CORS가 없다면 해커는 유저의 정보와 같은 민감한 정보를 조작된 요청을 통해 마음껏 빼낼 수 있다. 이러한 시도는 CSRF(Cross-Site Request Forgery) 혹은 XSS(Cross-Site Scripting)과 같은 여러 방법으로 이루어진다.

SOP

SOP(Same-Origin Policy)는 자원요청에 대한 또 다른 정책이다. SOP를 요약하자면 데이터 요청 혹은 리소스 공유는 같은 Origin에서만 발생해야 한다는 의미이다. CORS는 SOP의 예외조건이라고 생각하면 된다.

Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing.
RFC 6454 - 3.4.2 Network Access에서 발췌

CORS의 동작 방식

우리가 만약 브라우저를 통해 게시글의 목록을 서버 요청 한다고 생각해보자. 위 시나리오에서 클라이언트는 서버에 GET method를 통해 요청을 보낼 것이다. 이러한 요청은 마치 하나의 요청처럼 보이지만, 사실은 preflight 방식을 통해 두 번의 요청(예비요청, 본요청)으로 이루어진다.

Preflight Request

예비요청은 Preflight라고도 불리며, HTTP OPTIONS method를 통해 이루어진다. 예비요청의 역할은 본 요청을 보내기 전에 해당 요청이 정당한지 검사하는 것이다. 이 때 요청의 헤더에 Origin을 담아 CORS 정책을 만족하는지 검사한다.

이러한 과정을 플로우 차트로 나타내면 다음과 같다.

자바스크립트의 fetch API를 통해 먼저 브라우저에게 리소스를 받아오라고 명령을 내리면 브라우저는 OPTIONS 메서드를 통해 서버에 요청의 메타데이터를 전달한다. 서버는 이 preflight request의 응답으로 어떤 Origin을 허용하고 어떤 Resources를 허용하는지에 대한 정보를 전송한다. 응답이 성공적으로 이루어졌다면 Status Code 200이 전달된다. 그렇다고 해서 해당 요청이 정당하다는 의미는 아니다.

브라우저는 서버의 예비요청 응답을 보고 SOP 혹은 CORS 정책을 만족하는지 검사하고, 정당하다면 본요청을 수행한다.

OPTIONS 요청은 아래와 같은 정보를 담고있다.

OPTIONS https://localhost:8000/boards

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,ko;q=0.8,ja;q=0.7,la;q=0.6
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET
Connection: keep-alive
Host: evanmoon.tistory.com
Origin: https://localhost:3000
Referer: https://localhost:3000
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site

요청 헤더가 담고 있는 내용을 살펴보면 Origin 말고도 다른 정보도 함께 포함되어 있는 것을 알 수 있다. 이중 주목해야 할 항목으로는Access-Control-Request-Headers, Access-Control-Request-Method가 있다. 전자는 사용자가 지정한 요청해더, 후자는 요청 Method이다. 참고로 Postman에서 요청을 테스트하는 경우에는 Prelight Request를 보내지 않고 바로 본요청만 보낸다.

Simple Request

Simple Request는 예비요청 없이 바로 본요청만 보내는 것을 의미한다. Simple Request가 가능한 조건은 아래와 같다.


1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
3. 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.


Credential Request

마지막 시나리오는 인증된 요청을 사용하는 방법이다. 이 방법은 다른 출처에 요청할 때 보안을 더욱 강화한 형태이다. 기본적으로 브라우저가 제공하는 비동기 리소스 요청 API XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키와 같은 데이터를 함부로 요청 헤더에 담지 않는다. 이때 쿠키와 같은 데이터를 요청에 담을 수 있게 해주는 옵션이 credentials 옵션이다.

Credential Request를 하기 위해 충족해야 하는 조건은 아래와 같다.


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


CORS처리

CORS에러에 대한 처리는 일반적으로 백앤드에서 이루어진다. 물론 프론트에서 프록시 설정을 통해 해결할 수도 있지만 백앤드에서 처리하는 것이 더 일반적이다.

백앤드에서 CORS처리를 하는 방법 역시 사용하는 프레임워크에 따라 다르다. 필요한 경우 구글에 검색을 해보도록 하자. 보통 구글에 검색해서 나오는 자료들로 문제들을 대부분 해결할 수 있지만, 늘 그렇듯 예외는 존재한다. 만약 예상치 못한 문제가 발생한 경우 위에서 서술된 여러 CORS 에러 발생 조건들을 따져본다면 문제 해결에 분명 도움이 될 것이다.

0개의 댓글