이번시간에는 CORS이 무엇인가에 대한 포스팅을 시작하겠습니다.
웹 개발을 하다보면 CORS 정책위반으로 인한 에러가 발생하는 상황은 흔해서 누구나 한번쯤 겪게 된다고 해도
과언이 아닙니다.
주니어 프론트엔드 개발자들에게는 CORS의 문제가 어렵다는 것은 인지하고 있을 것입니다.
CORS에 대한 문제점은 초기 개발자들이 많은 오해가 일어날 수 있는데 CORS를 해결하는 방법은 이미 존재하지만
그것들을 잘 모르는 경우도 많은 것 같다.
CORS에 대한 사람들이 쉽게 오해할 만한 몇가지를 바로잡고, CORS로 인한 오류 해결방법을 제시해보겠다.
CORS에 대한 몇가지 오해
프론트엔드 개발만 해본 웹개발자라면 해볼만한 CORS에 대한 오해가 몇가지 있다. 이것들을 풀고나서 해결방법을 알아보자.
오해 1 : CORS 에러는 서버쪽에서 보내는 에러다.
CORS는 CSRF라고 불리는 브라우저 취약점 공격으로부터 브라우저 사용자를 보호하기 위해만든 기능이다.
HTTP 표준 스펙에 포함되 있고, CORS 관련 기능의 구현은 브라우저 개발사에게 책임이 있다.
웹 표준 기술을 따르는 정삭적이고 일반적인 브라우저라면 CORS 역시 표준 스펙이 맡게 구현되어 있다.
여기서 일반적인 브라우저라 함은, Exploer(익스플로러) 브라우저를 제외한 크롬, 사파리,
파이어폭스, 오페라 등을 말한다. 즉, 평범한 일반 사용자가 손쉽게 다운로드 받아 쓸수
있는 종류의(IE를 제외한) 브라우저들 이다. 크로미움 기반의 브라우저 중 중요한 보안정책들
을 의도적으로 비활성 시킨 버전의 특이한 브라우저들도 존재하는데, 이경우 CORS 에러가
발생하지 않을 수도 있다. (단, 이건 CORS 에러에 대한 올바른 해결방법은 아니다.)
CORS에 정책은 서버에 저장되어있고, 저장된 CORS 정책을 브라우저에게 보내주는 일을 서버가 담당하고 있긴 하다.
하지만, 그 CORS 정책을 보내달라고 서버에게 요청하는건 브라우저이다.
- 먼저, 브라우저에게 http요청이 발생한다면 브라우저는 발생한 http요청이 CORS검증을 해야하는 상황인지 판단한다.
- 보안 정책상 검증이 필요한 상황에 해당하면 CORS 검증을 서버에 요청한다. (Preflight Request)
- 서버에게 응답받은 CORS 검증 요청 결과에 따라서 브라우저는 발생한 http요청을 취소시켜버리고 에러를 뱉는다.
즉 , CORS는 브라우저에서 발생한다.
오해 2 : 외부 로부터 오는 공격은 서버를 보호하기 위해 특정 도메인 / IP 이외의 경로로 들어오는 요청을 차단하는 방식이 CORS 에러이다.
CORS는 보안정책중 하나지만, 보호의 대상은 서버가 아니다.
CORS는 HTTP 요청 헤더 중에서 Origin 이라는 헤더와 관련되어 있다.
브라우저에서 ajax 요청일 발생할 시. 현재 페이지의 주소창에 적힌 도메인과, ajax 요청 대상 url의 도메인이 같은 경우도 있지만 다른 경우도 있다. 전자의 상황은 SOR(Same Origin Request: 동일 출처 요청) 이라고 부르며
후자를 COR(Cross Origin Request: 교차 출처 요청) 이라고 부른다.
브라우저에서 cross origin 요청이 발생할 시 브라우저는 해당 요청에 origin 헤더를 자동으로 붙인다. 현재 페이지의 도메인 Origin 헤더의 값이 된다.
http://www.abc.com/page/1 페이지에서 http://www.abc.com/api/products 로 ajax 요청이 발생할
경우 요청시 현재페이지와 요청 대상의 도메인이 같으므로 same origin request(동일 출처 요청)
이라고 할 수 있다. 브라우저는 해당 요청의 헤더에 origin 헤더를 자동적으로 추가하며, 그 값은
http://www.abc.com이 된다.
그런데 보통 서버에 들어오는 요청들은 브라우저에서만 오는게 아니다. 서버끼리도 서로 요청을 한다.
브라우저를 통하지 않는 서버간 통신할 경우 Origin 헤더는 자동으로 붙는것이 아니다. 그렇다고 못붙이는 것도 아니다.
서버간 통신일때 요청자 서버는 원하면 Origin 헤더를 안 넣어도 되고, 넣고 싶으면 Origin의 값으로 아무 문자열이나
마음대로 넣어서 보낼 수 있다. 즉 서버간 요청 상황에서 CORS 정책은 보안상 의미를 가지지 않는다.
서버가 특정 요청자에게만 요청을 허용해야 할땐, 인증 토큰이나 보안키 등 더 확실하고 간단한 방법을 채택
오해 3 : 프론트엔드에서 AJAX 요청을 보낼때 어떤 특별한 라이브러리를 사용하거나 어떤 설정을 잘 해서 요청에 성공한다면 CORS 문제를 해결할 수 있다.
만일 어떤 라이브러리나 설정을 통해서 ,CORS 에러를 우회할 수 있는 방법이 있다면, 그것은 브라우저의 보안 취약점이다. 프론트엔드 소스코드만으로는 CORS에러를 우화하는 방법은 없어야한다.
만일 어떠한 프론트엔드 기술만으로도 CORS에러를 우회하는 방법을 발견했다면, 반드시 해당 브라우저를 개발한
기업또는 오픈소스 커뮤니티에 알려줘야한다. 취약점을 찾으면 그것을 수정할 것이다.
CORS의 배경과 필요성
CORS가 없던 과거
만약 이런 가정을 해봅시다.
짱구네 집이 자동문이라면 어떨까요?
아무나 짱구네 집에 들어갈 수 있을 것입니다.
그렇지만, 누구도 짱구네 집이 아니라 말할 수 없습니다.
주소는 엄연히 짱구네 집이니까요.
따라서, 브라우저 역시 마찬가지랍니다.
클라이언트는 브라우저라는 자동문을 통해 URL 주소로 누군가가 만들어 놓은 웹 사이트를 이용합니다.
특히 예전에는, 누구나 데이터를 요청하고, 응답할 수 있었습니다.
SOP의 탄생
하지만 이러한 시대의 평화는 오래가지 못했습니다. 보안에서의 문제점이 발생했기 때문입니다.
짱구(클라이언트)가 은행 사이트에 방문했다고 칩시다.
짱구는 워낙 호기심 많은 터라, 실수로 해커가 만든 웹사이트를 눌렀습니다.
그렇다면 어떻게 될까요?
해커가 원하는 요청대로 클라이언트가 요청한 것으로 이루어지는 보안상 문제점이 발생해버립니다.
이를 CSRF 공격이라고도 합니다.
따라서 브라우저의 고민은 깊어져만 가는데. 누구나 안전하게 브라우저를 이용하도록 말이죠!!
그래서 브라우저는 단 한 가지 대안을 찾게 됩니다. 바로 출저에 대한 엄격한 비교입니다. 출처에
대해서 완벽하게 같아야 데이터를 응답해주는 보안 정책, 이를 SOP 라고 합니다.
출처란?
그렇다면 우리는 출처란 무엇인가에 대해 알아야 합니다.
출처라는 것은 사실 엄연히 브라우저쪽에서 판단합니다.
Q: 브라우저마다 기준이 다를 수 있겠네요??
A: 그렇습니다. (ex: IE브라우저에서는 prot를 비교하지 않음!)
그렇지만, 그래도 어느 정도는 기준이 비슷합니다.
출처는 다음과 같이 Schme, host, port를 통해 비교를 합니다.
주의할건, port의 경우 생략되어 보여도,
http, https scheme마다 port의 기본값이 있음에 주의하자!
만약 이 3개가 같다면 같은 출처, 아니라면 다른 출처라는 것!
SOP
따라서 SOP는 이렇게 요청한 출처와, 응답할 출처가 같은 곳인지를 비교합니다.
쉽게 말하자면, 하도 짱구가 사고를 많이 치니, 브라우저가 짱구 엄마를 고용한겁니다.
아무리 짱구가 사고를 친다 해도, SOP가 적용된다면, 이제 다른 출처로 인한 공격에 덜 치명적이게 됩니다.
비록 짱구(클라이언트가) 혼나게 되지만, 적어도 예기치 않은 위험한 공격에 당하지는 않는다는 것!
SCP의 한계
그렇게 평화로웠던 브라우저, 하지만 개발자들은 점차 욕심이 생겼습니다.
어떻게, 다른 웹사이트의 유용한 API를 주고받을 수 있을까?
API 제공자는 이를 통해 이득을 취할 수 있으며,
사용자는 원하는 데이터를 제공받을 수 있습니다.
따라서 개발자들은, 꼼수를 쓰게되는데
그것이 바로 JSON등의 트릭방식 인데요.
결과적으로 이러한 꼼수를 통해 다른 출처임에도 우회하여 사용할 수 있는 것입니다.
우리의 브라우저는 다시 고민에 빠지게 되는데. 그 이유는 SOP가 보안 측면에서는 꽤 좋은 대안이지만
오히려 요청을 제대로 하지 않고 우회하는 방식으로 하는 이상한 문화가
생겨버리게 되었다.
그 꼼수들의 대해서는 다음링크를 참조하실 수 있습니다.
따라서 CORS라는 것이 생긴 것 입니다!
CORS란?!
이제는 짱구네 집에 놀러갈 때, 이전까지는 출처가 다르면 보내지를 않았습니다.
하지만 이제 철수가 놀러와도, 짱구네 집에서 훈이가 온다는 것을 인지하고 있다면 보내주는 것!
마치 자동문 앞에 호출기로 확인하는 것 같습니다!
즉, 다른 출처여도, 이미 예상되는 출처라면 서버에서 허용해주는 응답 헤더를 보내서, 브라우저가 응답 결과를 보내주는 겁니다.
이를 CORS(Cross Origin Resource Sharing)이라 해요.
결과적으로 보안은 SOP보다는 최소화된 보안 정책이지만, 세팅만 잘 해준다면, 그래도 출처가 다르더라도, 이상한 꼼수 없이, 데이터를 주고받도록 브라우저가 새로운 보안 정책을 마련한 것 입니다!
CORS의 작동 방식
설명 전 우리가 반드시 알아야 할 것이 있는데
⭐ CORS는 브라우저에서 출처를 비교하고 판단한다.
브라우저가 비교하면서 결과적으로 동작 방식에 따라서 다음과 같이 나누어 요청을 수행한다.
- simple requests (단순 요청)
- preflighted request (사전 요청)
simple requests(단순 요청)
쉽게 말해, 그냥 한 번 요청과 응답을 주고받는 것.
대신, 그만큼 안정성을 보장할 수 있도록, 엄~청 까다롭게 요청 조건을 세팅해놓았는데, MDN에 따르면 다음과 같다.
preflighted request(사전 요청)
simple requests로 요청을 하지않았다?! 싶다면 나머지는 바로 사전 요청으로 진행된다.
일단 preflight라는 말에서 미리 날린다는 느낌이다.
- 미리 한 번 찔러보고,
- 다음에 본 요청을 수행하는 방식으로 2번 요청
마치! 미리 놀러가기 전에(본 요청) 놀러가도 되는지 전화를 거는 것!(사전 요청)
상세한 과정은 다음 그림과 같다.
좀 더 부연 설명을 거처 보자면 MDN예시에 따라 다음과 같은 요청를 한다면
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resoureces/post-here/');
xhr.setRequestHeader('Ping-other', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
이렇게 요청과 응답이 나온다. 주요 응답 헤더 설명은 다음 그림에 제시되어 있다.
특이한 건 Max-age라는 응답 헤더가 있다.
이는 아무래도 사전 요청이 2번 이뤄지기 때문에, 서버의 부하를 최소화하기 위해 해당
origin과 header에 대한 응답을 캐싱해주는 기간을 의미한다.
잠깐! 왜 두번 요청하는지?
이상하게 느껴질 수 있다. 어찌 보면 2번 요청한다는 건 서버 측에서도 적지 않는 부담이 될 수 있다.
하지만, 이는 어쩔 수 없는 선택이기도 하다.
바로 브라우저가 출처를 비교한다는 맹점이 존재하기 때문이죠.
이 세상 서버들이 다 CORS를 인지하면 좋겠지만, 간혹 CORS를 모르는 서버들이 있으 수 있다. 그러면 다음과 같은 처리가 발생하게 된다.
결과적으로 브라우저는 거부했다 하지만, 이미 서버는 처리해버리는 엉뚱한 결과가 생겨버린다.
따라서 모든 서버가 안전하게 요청을 주고받을 수 있도록,
이렇게 2번 요청하는 사전 요청이 필요한 겁니다!
credentials에 따른 분류.
이렇게 동작 방식을 살펴봤는데요, 인증 정보 및 쿠키를 전송하는 지에 대한 여부에 따라 또 CORS는 인증 정보를 포함한 요청으로도 나뉜다.
일반적으로 브라우저는 쿠키와 인증 정보에 관해 매운 민감하다고 생각하기 때문에 함부로 보내주지 않는다.
하지만 보안상 좀 더 빡빡하게 해주었다면, 이를 허용해줘야한다.
조건은 다음과 같다.
- 서버에서 허용하는 출처를 "*"가 아닌 직접 명시를 해주며
- credentials : true를 허용하는 응답 헤더를 설정해주며
- 클라이언트 같은 경우 credentials 옵션을 넣어주는 경우
이때, option은 다음과 같이 3가지가 있다.
1. omit: 모든 쿠키, 인증 정보 교환 금지!
2. same-origin: 동일 출처에 한해서는 허용(기본값)
3. include: 포함
이때, 단순 요청 중 GET 메서드는 금지라고 한다!.
이러한 조건들을 통과하면, 비로소 쿠키를 주고받을 수 있는 것이다.
자, 이제 우리는 CORS의 3가지 종류를 살펴 보았다.
생각 보다 그렇게 어렵지 않다!
1. 그저 서버가 허용하는 출처를 만족하였는지,
2. 혹은 옵션과 헤더를 잘 설정해줬는가를 잘 판단해주면 된다.
그럼 우리는 이제 요청할 때에 있어 CORS방식에 맞춰 문제를 해결하는 법을 알게 되었다.
그렇지만 제대로 된 방식이더라도 이를 해결하지 못한다면, 다음과 같은 해결 방법을 사용하면 된다.
문제 해결 방법
1.서버 개발자와 빠르게 소통한다!
사실 이게 가장 바람직한 방법이다.
정말 우아한 설정을 통해 요청을 보낸다 해도
1.출처가 애초부터 허용되지 않도록 설정 되었다면
2.옵션과 응답 헤더를 깜빡하고 서버 개발자가 세팅해주지 않았다면
결과적으로 브라우저는 클라이언트의 요청이 정상적이라고 판단하지 않을것이고
따라서 모든 해결방법에 앞서, 일단 먼저 우리는 서버 개발자와 빠르게 소통해야 한다.
만약 그 서버가 우리 서버라면, 앞으로의 예기치 않는 서버 세팅 문제까지 해결해줄 수 있으니, 일석이조!!
2. 개발 환경 프록시 설정
1. 개발 환경에 있어 세팅을 잘 해놓은 상태거나
2. 서버에 세팅은 완벽하거나
그럼에도 불구하고 문제가 발생한다면, 개발 환경에서 프록시 설정도 대안이 될 수 있다.
이는 CRA, Vue-cli, Webpack-dev-server 등을 통해 세팅을 직접해 줄수 있는데, 통일되지 않고 각자마다
방법이 다르다.
따라서 공식문서들을 통해 따로 설정해주면 된다.
3. 프록시 서버 구축
아무래도 모든 것들이 안 된다면, 우리는 프록시 서버를 구축해야 할 것이다.
이것이 가능한 이유는, CORS는 브라우저에서 판단한다고 했는데,
따라서 브라우저를 거치지 않는 서버간 요청은 CORS를 따지지 않는다는 것이다.
좀 더 이해하기 쉽게 말하자면, 철수가 짱구와 연락할 수 없는 상황일 때, 흰둥이를 통해
짱구와 연락을 주고 받는 것!!
따라서 이렇게 서버를 구축하면 가능할 수 있겠지만, 문제는
1. 추가로 서버를 세팅해야 한다는 한계점과
2. 이로 인한 시각적, 인력 자원의 소요
가 있다. 따라서 만약 프록시 서버가 이미 구축되어 있지 않다면,
이를 위해 사전에 해결할 수 없는지 미리 고민 해봐야 한다.
4.Access-Control-Allow-Origin response 헤더를 추가
app.get('data', (req, res) => {
res.header("Access-Control-Allow-Origin", "*");
res.send(data);
});
간단하게 모든 클라이언트에 요청에 대한 cross-origin HTTP 요청을 허가하는 헤더를 추가해 주었다.
그러나 rest.api의 모든 응답에 일일이 추가하는 점이 불편할 수 있다.
5.node.js의 미들웨어 CORS 추가
이미 만들어진 node.js 미들웨어 중 이를 해결해주는 미들웨어가 있는데 바로 CORS 이다.
npm install --save cors
yarn add cors
이것을 이용하면 더욱 간단하게 CORS를 허가해줄 수 있다.
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors()); // CORS 미들웨어 추가
...
하지만 위에 처럼 헤더를 추가 하거나 미들웨어를 적용하면 모든 요청에 대한 허가를 하게 된다. 보안적으로 취약점이 생기는데 cors 미들웨어는 여러가지 를 설정할 수 있다.
( https://www.npmjs.com/package/cors )
const corsOptions = {
origin: 'http://localhost:3000', // 허락하고자 하는 요청 주소
credentials: true, // true로 하면 설정한 내용이 response 헤더에 추가된다.
};
app.use(cors(corsOptions)); // config 추가
정리
결과적으로 CORS에 있어 다음 사항만은 유의해야한다.
- CORS는 완벽하지 않은, 최소한의 보안 정책으로, 정말 잘 세팅하는데 주의를 기울여야 한다.
- CORS의 비교 주체는 브라우저라는 점을 꼭 명심해줘야 한다! 이에 대한 조건 충족 여부에 따라 단순 요청, 사전 요청 등을 자동으로 수행하기 떄문이다.
- 소통이 가능한 환경이라면 해당 서버 개발자와 협업을 통해 문제를 해결하는 것이 가장 바람직한 것!
CORS가 나온다고 망설일 필요가 없다. - CORS이란 도메인 또는 포트가 다른 서버의 자원을 요청하면 발생하는 이슈!
- 서버와 클라이언트가 분리된 앱에서는 cross-origin HTTP 요청을 서버에서 승인해주는 것 이 좋다!
참고 자료
- CORS를 처음 마주하는 분들에게
- CORS에 대한 간단한 고찰
'웹개념' 카테고리의 다른 글
[HTTP] HTTP 상태코드 (0) | 2022.01.16 |
---|---|
Cookie vs LocalStorage vs SessionStorage (0) | 2021.10.12 |
동기와 비동기 개념과 차이 (1) | 2021.10.09 |