치춘짱베리굿나이스

CORS 본문

이론적인 부분들/웹

CORS

치춘 2023. 6. 17. 17:34

'localhost:9000' 출처로부터 발생한 'locahost:8080/graph' 를 향한 fetch 접근은 CORS 정책에 의해 차단되었습니다: 
Preflight 요청에 대한 응답이 접근 제어 체크를 통과하지 못하였습니다: 
'Access-Control-Allow-Origin' 헤더가 요청한 리소스에 존재하지 않습니다. 
불투명한 응답이 필요한 경우, 요청 모드를 no-cors로 설정하여 CORS가 비활성화된 리소스를 fetch하세요.

많은 프로젝트들을 좌절케 하는 그 녀석이다

나도 프론트엔드 처음 공부할 때 이 녀석 때문에 프로젝트 하나가 터진 적이 있었다 (크흠,,,)

그 때의 과오를 잊지 않기 위해 정리하기…

사전 지식

도메인, 스킴, 포트

CORS를 보기 전에 도메인, 스킴, 포트가 무엇인지 알아보자

  • 스킴 (Scheme)
    • 웹 사이트 접근을 위해 사용되는 프로토콜 (통신 방식) 을 정의하는 부분이다
    • 브라우저는 이곳에 명시된 프로토콜 (http, https, ftp 등…) 을 이용하여 웹 사이트 서버와 통신하여 페이지를 불러온다
  • 도메인 (Domain)
    • 각 웹 사이트들은 ip 주소 (123.456.789.0) 로 식별하는데, 이는 사람이 이해 및 기억하기 어렵기 때문에 이에 1대 1 대응하는 이름을 부여한 것이 도메인이다
    • 우리가 아는 naver.com, notion.so 등이 도메인이라고 할 수 있다
  • 포트 (Port)
    • 도메인 (사실상 ip 주소) 에 해당하는 장치 (PC) 에 접속할 수 있는 입구를 명시한 부분이다

URL을 구성하는 위의 3개를 묶어서 ‘출처’ (Origin) 이라고 하는데, CORS (교차 출처 리소스 공유) 란 이 출처를 바탕으로 동작하는 매커니즘이다

SOP

동일 출처 정책(same-origin policy)은 어떤 출처에서 불러온 문서나 스크립트가 
다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. 
동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.

Same-Origin Policy (동일 출처 정책)

서로 다른 출처를 가진 리소스가 보안상의 이유로 상호작용 (요청 - 응답 등) 을 하지 못 하도록 하는 것이 동일 출처 정책이다

쉽게 말하면, A라는 출처를 가진 웹 사이트는 A라는 출처를 가진 서버에서만 요청을 보내고 응답을 받아올 수 있으며, B라는 출처를 가진 서버에서 데이터를 받아오는 것은 정책적으로 막혀있다는 뜻

만약 다른 출처에서의 데이터를 모두 허용해 버린다면? 요청이 해커 등에 의해 중간에 탈취당해 이상한 사이트로 날아가거나, 알 수 없는 사이트에서 응답이 날아오는 등의 보안상 허점이 발생할 수 있으며, 이를 위해 등장한 정책이라고 할 수 있다

 

스킴, 포트, 도메인 중 하나라도 다르다면 동일 출처가 아니라고 판단하며, 데이터를 주고받는 것이 금지되어 있다

  • 현재 웹 사이트 (blog.chichoon.com) 는 같은 출처를 가진 서버 (blog.chichoon.com, 포트번호도 같아야 한다) 에서만 데이터를 받아올 수 있다
  • blog.chichoon.com/api 에서는 데이터를 받아올 수 있지만, api.chichoon.com 에서는 데이터를 받아올 수 없다

하지만 웹 사이트가 여러 서버와 통신하여 데이터를 주고받고 교류하는 우리네 세상에서 SOP은 그야말로 깐깐한 정책이 아닐 수 없다…

이를 완화하기 위해 등장한 것이 이번에 알아볼 CORS이다

CORS

MDN 에서의 서술

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) 
other than its own from which a browser should permit loading resources. 
CORS also relies on a mechanism by which browsers make a "preflight" request to the server hosting the cross-origin resource, 
in order to check that the server will permit the actual request. 
In that preflight, the browser sends headers that indicate the HTTP method and headers 
that will be used in the actual request.
교차 출처 리소스 공유 (CORS) 는 서버로 하여금 브라우저에게 자신의 출처 (도메인, 스킴, 포트) 를 제외한 
모든 출처로부터 리소스를 받아올 수 있도록 허용케 하는 HTTP 헤더 기반의 매커니즘입니다.
CORS는 브라우저가 실제로 요청을 허용할지 확인하기 위해 교차 출처 리소스를 호스팅하는 서버로 "preflight" 요청을 보내는 메커니즘에도 의존적입니다. 
prefilght 요청에서는 브라우저가 실제 요청에서 사용될 HTTP 메서드와 헤더를 담은 헤더를 전송합니다.

CORS란?

Cross-Origin-Resource-Sharing (교차 출처 리소스 공유)

SOP (동일 출처 정책) 이 서로 다른 출처간 리소스 교환을 막는 정책이라면, CORS는 HTTP 헤더를 이용하여 브라우저에 서로 다른 출처간 리소스 접근을 허용할 수 있도록 하는 정책이다

CORS 허용 여부는 브라우저가 결정하며, 모든 브라우저가 기본적으로 CORS를 허용하고 있지 않기 때문에 우리는 맨 위의 사진과 같은 시뻘건 오류와 마주하는 것이다

우리가 마주하는 CORS 오류는 이 정책의 요구사항을 지키지 않아 발생하는 오류이고, 정작 CORS 자체는 우리에게 고통을 주려는 의도가 아니라 오히려 빡빡한 SOP 정책을 말랑말랑하게 만들어주는 착한 녀석이었던 것 (에러 문구에 CORS라고 나오니까 괜히 CORS 탓으로 보였던 것…)

CORS 정책을 사용하는 요청

  • XMLHttpRequest
  • Fetch API
  • 웹 폰트
  • WebGL 텍스쳐
  • drawImage() 를 통해 캔버스에 그린 이미지 또는 비디오 프레임

CORS 정책에 의해 제한되지 않는 요청

  • 간단한 GET 요청
    • 특정 Content-Type을 가진 데이터에 한해서만 적용된다
    • ex: application/x-www-form-urlencoded, multipart/form-data, text-plain
  • HEAD 요청
    • 응답으로 전체 리소스 컨텐츠를 받지 않고, 리소스의 헤더만을 받기 때문에 CORS 이슈에 걸리지 않는다
  • 특정한 POST 요청
    • 특정 Content-Type을 가진 데이터에 한해서만 적용된다
    • ex: application/x-www-form-urlencoded, multipart/form-data, text-plain
    • form 태그의 기본값을 이용한 데이터 주고받기는 GET, POST 모두 CORS 정책에 걸리지 않는다고 보면 된다
    • multipart/form-dataform 태그를 이용하여 파일 (이미지, 미디어 등…) 을 주고받을 때 사용된다
  • <script> 태그를 통핸 자바스크립트 실행
  • 이미지 렌더링
  • <link> 태그를 이용한 스타일시트 로딩
  • @font-face 를 통한 폰트 로딩

PUT, DELETE 등의 요청은 GET, POST에 비해 간단한 요청으로 취급되지 않기 때문에 CORS 정책에 위배될 수 있다

CORS 정책 적용 과정

  • 브라우저는 요청을 보낼 때 헤더에 출처 (Origin) 정보를 담는다
    • 여기에는 요청이 생성된 곳의 출처 (도메인, 스킴, 포트번호) 가 명시되어 있다

 

  • 브라우저에서는 본 요청 (PUT) 전에 Preflight (OPTIONS) 요청을 보낸다
    • 프리플라이트 요청은 CORS 정책을 적용받는 요청에 대해서, 본 요청을 보내기 전 브라우저 측에서 추가로 보내는 요청이다
    • 프리플라이트 요청에는 추가적인 헤더 정보가 담겨있으며,
      • Access-Control-Request-Method: 교차 출처 요청 전, 서버에게 실제 요청에서 어떠한 메서드 (GET, POST 등) 가 사용될 지 명시해 주는 헤더. 예시에서는 POST가 사용된다고 명시되어 있다
      • Access-Control-Request-Headers: 서버에게 실제 요청에서 어떠한 헤더가 사용될 지 명시해주는 헤더. 예시에서는 authorization, content-type 헤더가 사용된다고 명시되어 있다
      • Origin: 요청을 보내는 주체의 출처. 스킴, 도메인, 포트번호가 포함된다
    • 프리플라이트 요청은 브라우저에서 알아서 만들어 보내므로 프론트엔드 측에서 추가적으로 작업해줄 필요는 없다

 

  • 서버에서 프리플라이트 요청에 대한 답장을 보낸다
    • 마찬가지로 프리플라이트 응답에는 추가적인 헤더 정보가 담겨있으며,
      • Access-Control-Allow-Origin: 프리플라이트로 날아온 Origin이 실제 요청에서 리소스에 접근 가능한 출처인지 응답하는 헤더. 모든 출처에 대한 응답을 허용할 경우 이 부분이 * (와일드카드) 로 되어 있다. 여기에서는 http://localhost:3000이 리소스에 접근가능한 출처임을 명시하고 있다
      • Access-Control-Allow-Methods: 실제 요청을 통해 리소스에 접근 시 사용가능한 메서드를 알려주는 헤더이며, Access-Control-Request-Method에 대한 답장. 여기에서는 GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH가 사용가능하다고 명시하고 있다
      • Access-Control-Allow-Headers: 실제 요청에서 사용가능한 헤더를 알려주는 헤더이며, Access-Control-Request-Headers에 대한 답장. 여기에서는 authorization, content-type이 사용가능하다고 명시하고 있다
      • Access-Control-Max-Age: 위의 헤더 값들 (Allow-Methods, Allow-Headers) 이 얼마나 오랜 기간 캐싱될 수 있는지를 명시하는 헤더
    • 우리가 마주하는 CORS 이슈는 이 단계에서 발생하는 것으로, Access-Control-Allow-Origin에 우리가 프리플라이트 요청을 보낸 출처 값이 명시가 되어 있지 않아 브라우저에서 CORS를 비허용하는 것으로 판단하여 차단하는 것이다
  • Acess-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods 을 통해 실제 요청으로 전송될 출처메서드, 헤더 값이 CORS 허용된 값이라고 판단될 경우, 본 요청을 보낸다
  • 응답을 받는다

Credentials와 함께하는 CORS 정책 적용

쿠키나 Authorization 헤더를 이용하여 Credential이 포함된 요청을 보내기 위해서는, 위의 단순 요청과 다르게 Access-Control-Allow-Credentials라는 헤더가 추가로 들어가며, 프론트엔드 측에서도 추가적으로 설정해줘야 할 옵션이 있다

 

  • 프론트엔드 측에서는 요청을 보내기 전에 credentials (fetch), withCredentials (axios) 옵션을 켠다
    • 이 설정을 해 주지 않으면 헤더에 쿠키 등의 크레덴셜 정보가 포함되지 않은 채로 요청이 날아가게 된다

 

  • 응답의 Access-Control-Allow-Credentials 헤더는 true로 되어 있어야 크레덴셜 값을 받을 수 있다
  • 응답의 Access-Control-Allow-Origin 헤더에는 *이 아닌 출처가 명시되어 있어야 한다
    • 당연하지만 요청이 보내진 출처와 다른 출처가 적혀있을 경우, 브라우저 단에서 응답이 차단되고 CORS 이슈가 발생한다

임시방편: Proxy를 사용하여 프론트엔드 측에서 CORS 이슈 우회

// package.json
{
    "proxy": "https://localhost:3000" // 서버의 출처
}

CRA를 사용하여 개발을 수행한다면, 위와 같이 package.jsonproxy를 명시하여 개발 서버에서 프록시를 사용할 수 있다

프록시란? 클라이언트와 서버 사이에서 동작하는 중개자 역할의 서비스로, 클라이언트가 서버에 직접 연결하는 대신 프록시를 한번 거쳐 요청과 응답을 수행한다

프록시의 활용 방법 중 하나로 자기 자신의 출처를 다른 출처로 속여 익명성을 확보할 수 있는데, 이를 이용하면 출처를 서버에서 허용하는 (Access-Control-Allow-Origin에 명시된) 출처로 바꿔 CORS 이슈를 회피할 수 있다

리액트 개발 서버에서 지원하는 기능인 만큼, 실제 서비스에서는 위와 같은 헤더 설정을 다 해 주어야겠지만, 헤더 설정이 되어 있지 않을 서버 개발 단계에서는 프록시를 이용하여 간단하게 이슈를 회피할 수 있다

여담

이제 CORS가 통신 막는 나쁜 놈이 아니라 사실은 SOP를 유연하게 허가해 주는 착한 녀석이라는 것을 알았다

앞으로 CORS 이슈가 발생하면 SOP을 탓하도록 하자


참고 자료

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#명세

https://developer.mozilla.org/ko/docs/Glossary/Preflight_request

https://stackoverflow.com/questions/68675773/why-my-formdata-post-isnt-blocked-by-cors-policy

https://react.vlpt.us/redux-middleware/09-cors-and-proxy.html

'이론적인 부분들 > ' 카테고리의 다른 글

OAuth  (0) 2023.07.25
WebSocket  (0) 2023.07.21
DOM과 웹 렌더링  (0) 2023.05.19
JWT  (0) 2022.10.10
로그인, 인증, 인가  (0) 2022.10.08
Comments