윤일무이
[Web] CORS는 어디 사는 누구길래 나를 괴롭게 하나 본문
처음 백엔드와 협업을 하면서 마주했던 CORS 에러 메세지
CORS란 Cross Origin Resource Sharing, 교차 출처 리소스 공유라는 뜻이다.
교차 출처는 뭐고 이런 오류는 왜 나는지, 어떻게 수정해야 하는지 하나씩 공부한 내용을 정리해봤다.
1. CORS란
웹 브라우저에서 작동하는 보안 매커니즘 중 하나로, 웹 브라우저에서 동작하는 JS 코드가 다른 출처의 리소스에 접근하는 것을 제어하는 메커니즘이다. (여기서 다른 출처란 프로토콜+도메인+포트 번호를 말한다. 예컨대 http랑 https는 다른 프로토콜을 가지고 있다. 포트 번호도 80과 443으로 다르다.)
이러한 출처 비교와 다른 출처의 리소스일 시에 차단하는 역할은 서버가 아닌 브라우저가 하고 있다. 그래서 브라우저 -> 서버로 어떤 정보를 요청했고, 서버 -> 브라우저로 정보를 줄 때 '너 나랑 다른 출처임 니 정보 안 받을 거임'하고 뭔가 무시무시한 에러를 띄워주는 것이다.
그럼 왜 다른 출처의 리소스에 접근하는 것을 제어하는 것일까?
위에서 말했다시피 '보안'을 위해서 제어한다.
CSRF(Cross-Site Request Forgery: 크로스사이트 요청 위조)는 공격자가 사용자의 브라우저를 이용해 사용자가 의도하지 않은 요청을 다른 웹사이트에 보내는 공격이다. 예를 들어 사용자가 은행 웹 사이트에 로그인한 상태에서 악의적인 웹 페이지에 접속한다면, 사용자의 세션 정보는 그대로 유지되어 있기 때문에 이 악의적인 웹 페이지는 사용자의 브라우저를 통해 은행 계좌 이체 요청을 은행 웹 사이트에 보내는 스크립트를 실행할 수 있다. 즉 사용자의 동의 없이 서버에 어떤 요청을 보내고 개인정보를 가로챌 수 있는 것이다.
XSS(크로스사이트 스크립팅)은 공격자가 악의적인 스크립트를 웹 페이지에 삽입해 사용자의 브라우저에 실행되게 하는 공격이다. 이렇게 삽입된 스크립트는 사용자의 정보를 도용하거나 CSRF와 같이 사용자의 동의 없이 악의적인 작업을 수행하게 한다. CORS와 직접적인 관련은 없다지만, XSS 공격자가 삽입한 스크립트를 다른 도메인 서버로 보내 정보를 탈취하는 경우, CORS 정책이 제대로 설정되지 않은 경우 공격할 수 있다.
CORS 매커니즘이 없다면 CSRF나 XSS와 같은 보안 취약점이 발생할 수 있다.
여기까지 CORS가 뭐고 왜 필요한지를 이해해보았다.
다른 출처의 리소스에 접근할 수 있도록 제어하는 메커니즘, 보안을 목적으로 실행된다고 정리할 수 있다.
2. 브라우저의 CORS 기본 동작
웹 브라우저가 http 요청에 대해서 어떤 요청을 하느냐에 따라 차이가 있음을 먼저 정리하고 간다.
img, video, link 태그들은 src, href에서 다른 사이트의 리소스에 접근하는 것이 가능하다.
이러한 태그들은 보안 정책 상 스크립트보다 더 관대하게 처리되어 기본적으로 cross-origin, 교차 출처를 허용한다. 이러한 태그들은 웹 페이지 내에서 다양한 출처의 이미지나 비디오를 보여주는데 사용되므로 사용자 경험을 위해 유연하게 처리되기 때문이다.
반면 XMLHttpRequest, Fetch API 스크립트와 같이 HTTP 요청을 다루는 자바스크립트 라이브러리는 기본적으로 same-origin, 동일 출처를 지향한다. 그래서 다른 도메인의 리소스에 데이터를 요청하는 경우는 '같은 출처'여야 하는 것이다.
그럼 이제 웹 페이지의 스크립트가 다른 출처로 접근하거나 어떤 요청을 보내려고 한다.
1. 클라이언트에서 http 요청의 헤더에 Origin을 담아 전달한다.
2. 서버는 응답 헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 다시 전달한다.
3. 클라이언트에서 Origin과 서버의 Access-Control-Allow-Origin을 비교해 동일하다면 사용하고 동일하지 않다면 에러를 띄워보낸다.
이번 협업에서 나는 axios를 사용했다. axios는 기본적으로 브라우저의 CORS를 준수하는 방식으로 설계되어 있어 1번을 하지 않아도 잘 처리 된다. axios는 자동으로 현재 페이지의 출처를 파악해 요청 헤더에 추가하기 때문에 유저가 별도로 무언가를 설정하지 않아도 된다.
즉 axios를 사용했다면 기본적인 해결방식은 서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해 응답하면 되는 것라고 이해했다.
다만 위는 기본적인 동작 방식이고 공식 문서에서 말하는 CORS의 3가지 시나리오가 있어 찾아봤다.
- 예비 요청
- 브라우저는 요청을 보낼 때 한 번에 바로 보내지 않고 예비 요청을 보내 서버와 통신이 잘 되는지 먼저 확인한 후에 본 요청을 보낸다고 한다. 이러한 예비 요청을 Preflight라고 하는데, 이 예비 요청은 http의 메소드로 OPTIONS를 쓴다. 그래서 클라이언트 -> 서버로 출처나 요청에 사용할 메소드, 헤더들을 실어서 보내면 서버는 이에 대한 응답으로 헤더 정보를 담아 브라우저로 보내준다. 브라우저는 자신이 보낸 요청과 서버의 응답을 비교해 확인한 후 본 요청을 보내고, 서버는 본 요청에 대해 응답을 하는 구조로 진행된다.
- axios의 경우 역시 별도로 예비 요청과 헤더 설정을 신경쓰지 않아도 된다.
- 예비 요청을 통해 보안을 강화하는 것은 좋으나, 실제 요청까지 뎁스가 길어져서 어플리케이션의 성능에 악영향을 끼치는 단점이 있다고 한다. 브라우저의 캐시를 통해 예비 요청을 캐싱 시켜 최적화를 할 수 있다고 하는데 이 부분은 자세히 알지 못해 이 정도로 서술한다.
- 단순 요청
- 예비 요청을 생략하고 바로 본 요청을 보낸 것을 말한다. 단 단순 요청은 조건이 까다롭다.
- 요청의 메소드는 무조건 GET, HEAD, POST 중 하나여야 하며 특정 헤더의 경우에만 적용된다. 또한 Content-Type 헤더 역시 3가지 중 하나로 특정되어야 하며 이 세 가지 조건이 모두 만족되어야만 단순 요청이 일어난다.
- 조건이 까다롭다고 한 이유는 Content-Type에 일반적으로 많이 쓰는 json, xml이 아닌 다른 경우이기 때문이다.
- 이 부분도 내가 잘 아는 내용이 아니라 간단하게만 서술한다.
- 즉 단순 요청의 조건이 까다롭기 때문에 대개 예비 요청으로 진행된다.
- 인증된 요청
- 클라이언트 -> 서버로 자격 인증 정보(Credential)을 실어 요청할 때를 말한다.
- 자격 인증 정보란 세션 ID가 저장된 쿠키나 인가 헤더에 설정하는 토큰 값을 말한다.
- 이 부분은 잘 모르니 공부해보도록...
- 이런 인증 정보를 포함해서 다른 출처의 서버로 요청할 때는 Credentials 옵션을 설정해야 한다. 일반적으로 인증과 관련된 데이터는 함부로 요청 데이터에 담지 않도록 되어 있기 때문이다. 이 설정을 하지 않으면 절대 서버에게 인증 정보를 보내지 않는다.
- same-origin(같은 출처간 요청에만 인증 정보를 담는다)가 디폴트이고, include를 써줘야 모든 요청에 인증 정보를 담을 수 있다.
- fetch, axios등 어떤 라이브러리로 요청하느냐에 따라 사용방법이 다른데 나의 경우 axios는 withCredentials: true로 설정하면 됐다.
- 클라이언트 측에서 위와 같은 설정을 했다면 이제 서버에서도 인증된 요청에 대해 헤더를 설정해야 한다.
- 서버는 잘 모르지만 응답 헤더의 Access-Control-Allow-Credentials 를 true로 설정해야 하며, Origin이나 Methods, Headers 부분에는 와일드 카드를 사용하면 안된다. 인증 정보는 민감하기 때문에 정확한 출처를 기재해야 하기 때문이라고 한다.
3. 해결 방법...?
프론트엔드의 관점에서는 어떤 시나리오인지에 따라, 어떤 http 요청 라이브러리를 썼느냐에 따라 설정해야 하는 부분을 챙겨주면 된다. 이외에는 서버에서 허용을 해줘야 하기 때문에 클라이언트에서 무언가를 더 해결할 수 없다고 생각했다.
우리도 동일한 오류가 났는데, 내가 처음 ngrok을 써서 이것이 원인인지 무엇이 원인인지 원인 파악을 제대로 하지 못함이 가장 아쉬웠다. 중간에 한번 해결했었는데, 그 부분 역시 서버단에서의 어떤 컨트롤러? 인터셉터? 부분의 요인이 있어서 원인과 해결방법 모두 시원하게 이해하지 못했다. 결론적으로는 배포 후에도 이슈가 있어서 완전히 해결하지 못한 점도 찜찜하고.
다만 중간에 해결했을 때 백엔드 분의 브라우저에서는 해결되고 내 브라우저에서는 해결이 안되는 이슈가 있었다.
원인은 놀랍게도 나도 까먹고 있었던 CORS 확장 프로그램이었다.
이전에 백준 허브에 무슨 이슈가 있었는데 그걸 해결할 수 있는 방법으로 CORS 확장 프로그램을 깔라고 해서 깔았던 건데 이게 작동됨에 따라 내 브라우저에서만 해결이 되지 않았던 것이다.
그래서 이번을 통해 느낀 것은 늘 그렇지만 '컴퓨터는 헛소리를 하지 않는다'였다.
왜 안되지 왜 안되지 했는데 아 이걸 빼먹었구나! 아 이걸 안했구나 를 몇 번 마주하면서 컴퓨터는 항상 사실만을 말한다는 것이었다.
당장 CORS 에러 메세지도 히익 왜 이렇게 빨갛고 길어; 했는데 찬찬히 읽어보면 '너 지금 다른 도메인의 리소스에 접근하려고 하는 거임? 안됨 안됨' 이라고 말하고 있는 것이니 접근하도록 허용해줘! 하고 개발자가 허용을 해주면 되는 것이었다.
그런 고로 항상 컴퓨터의 말에 귀를 기울일 것, 안되면 내가 뭔가 놓쳤다고 생각할 것!
CORS... 또 만나게 되겠지...? ㅎㅎ 그 때까지 세션이나 인증 인가에 대해서 공부해놔야겠다.
**출처
교차 출처 리소스 공유 (CORS) - HTTP | MDN
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라
developer.mozilla.org
🌐 악명 높은 CORS 개념 & 해결법 - 정리 끝판왕 👏
악명 높은 CORS 에러 메세지 웹 개발을 하다보면 반드시 마주치는 멍멍 같은 에러가 바로 CORS 이다. 웹 개발의 신입 신고식이라고 할 정도로, CORS는 누구나 한 번 정도는 겪게 된다고 해도 과언이
inpa.tistory.com