웹/배경지식

CORS, SOP란?

한땀코딩 2021. 3. 13. 23:58

CORS 그 첫 만남

최근 프로젝트를 진행하면서 또 마주친 것이 있으니 바로 CORS 정책입니다. GET 요청은 문제없이 보내져서 막연하게 생각하고 넘어갔지만, POST 요청이 시작되면서 브라우저가 이런 경고창을 띄우기 시작했습니다.

url 이 나와서 가렸습니다

이 오류 메시지는 프론트와 백의 서버를 분리하여 배포하면서부터 처음 접했었는데, 이렇게 잊을만하면 마주치게 되는 것 같습니다. 웹 브라우저 기반으로 작업하는 프론트엔드 개발자라면 늘 신경 쓰고 백엔드 개발자와 소통해야 하는 부분이지 않나 싶습니다.

CORS 경고 메시지

위의 오류 메시지를 읽어보면 브라우저의 오리진으로부터 서버로 보내는 XMLHttpRequest 요청이 CORS 정책에 의해 막혔다고 안내하고 있습니다. preflight request의 응답에 Access-Control-Allow-Origin이 없다고 추가적으로 설명해주고 있습니다. 이전에는 제가 서버 또한 직접 만들었기 때문에, Express 기반의 서버에서 cors 라이브러리를 받아서 그냥 모든 브라우저 허용을 해주는 방식으로 해결을 했었습니다 (이렇게 하면 문제는 해결하겠지만 CORS가 없는 것이나 마찬가지라 민감한 정보가 오가야 한다면 절대 해선 안 됩니다). 그러나 프론트만 직접적으로 담당해보니 CORS 메시지를 좀 더 신중하게 읽어보면서 이 정책이 생긴 배경, 그리고 프론트엔드 개발자 입장에서의 해결 방법이 어떤 것이 있을지 의문이 생겼습니다.

관련 키워드

CORS를 나름대로 이해해보려면 단순히 CORS라는 단어만이 아니라 관련된 여러 용어들을 알면 좋다고 생각합니다. 나름대로 키워드 몇 가지를 뽑아봤는데 이를 중심으로 제가 공부한 내용을 정리해보려고 합니다.

  • CORS (Cross Origin Resource Sharing)
  • origin
  • SOP (Single Origin Policiy)
  • preflight request
  • Access-Control-Allow-Origin (response header)

CORS (Cross-Origin Resource Sharing)

CORS는 이름에서 보이듯이 서버와 브라우저의 오리진이 다를 때 브라우저가 보낸 리소스 요청을 처리하는 메커니즘입니다. 기본적으로 브라우저와 서버의 오리진이 다르다면 브라우저는 서버가 보낸 리소스를 사용할 수 없습니다 (이와 관련된 내용이 SOP입니다). 브라우저가 요청을 보내고, 이에 대해 서버가 보낸 응답의 http 헤더 중 Access-Control-Allow-Origin에 브라우저의 오리진이 포함되어있는지를 확인합니다. 만약 허용되지 않은 오리진이라면 우리는 위에서 본 빨간 오류 문구를 마주하게 됩니다.

Origin이란?

어떤 사이트의 url을 구성하는 것은 여러 가지가 있습니다. 관련해서 따로 포스팅을 작성한 적이 있으니 조금 더 상세한 내용이 필요하시다면 이 포스팅을 참고 부탁드립니다.

오리진이 같다는 것은 url에서 scheme(protocol), host(domain), port가 동일하다는 것을 의미합니다. scheme은 http, https 같은 프로토콜 명시 영역, host는 우리가 흔히 '주소'라고 부르는 사이트의 도메인(이름), port는 :(콜론) 뒤에 따라붙는 숫자입니다. 도메인이 같더라도 포트가 다르면 다른 사이트인 경우가 많죠. 그리고 http의 기본 포트는 80, https의 기본 포트는 443으로 포트가 명시되지 않은 사이트는 앞에 적힌 scheme에 따라 80, 443 포트를 생략하여 적을 수 있는 겁니다.

http://example.com/app1/index.html
http://example.com/app2/index.html

예를 들어 위의 두 url은 same origin입니다. 포트 80은 생략되고 있고, 나머지 scheme, host가 동일합니다.

http://example.com/app1
https://example.com/app2

그와 반대로 동일한 도메인명을 가지고 있지만 scheme이 다르기 때문에 위의 둘은 origin이 동일하지 않습니다. 참고로 cors와는 별개의 이슈지만, https 프론트와 api 통신을 하려면 서버 또한 https 대응이 되어야 합니다.

http://example.com:3000/index.html
http://example.com:5000/index.html

이 둘도 포트 번호가 다르기 때문에 다른 origin입니다.

SOP (Single Origin Policy) - 동일 출처 정책

CORS가 어떻게 동작하는지를 살펴보기에 앞서, 왜 CORS를 우리가 이용해야 하는지에 대해 잠시 탄생 배경을 이야기할 필요가 있을 것 같습니다. 브라우저는 기본적으로 오리진이 다를 경우, 스크립트의 실행을 제한합니다. 쉽게 말해서, 실수로 잘못 들어간 사이트에서 멋대로 코드를 실행하여 쿠키 같은 개인정보나, 다른 사이트의 DOM 조작을 하지 못하도록 막을 수 있는 것입니다.

하지만 모든 보안에는 trade-off가 있겠죠? 동일 출처 정책 때문에 이런 위험한 접근도 막을 수 있지만, 동시에 브라우저와 서버가 오리진이 다를 경우에도 리소스 사용이 차단됩니다. 이 때문에 우리는 서버가 응답을 보낼 때 CORS와 관련된 처리를 해줘야 하는 것입니다. 귀찮은 부분이 많다고 느껴지겠지만, 이런 안전장치가 없을 때 일어날 수 있는 여러 이슈를 생각하면 꽤나 아찔합니다.

CORS의 작동 방식

서로 origin이 다른 것을 브라우저와 서버가 어떻게 알게 될까요? 브라우저가 서버로 어떤 요청을 보낼 때, 오리진이 다른 경우 request header에 자신의 origin을 포함하여 보내게 됩니다. 그리고 이런 CORS 요청이 필요한 경우 브라우저에서는 간단한 요청 (simple request)를 제외하고는 preflight request를 먼저 서버 측으로 보내서 유효한 응답이 오는지 확인을 하게 됩니다. 확인은 응답 헤더의 Access-Control-Allow-Origin을 확인하는 등의 방법을 취하게 되고, 이 헤더에 현 브라우저의 오리진이 명시되어 있다면 유효한 요청으로 판단하여 CORS가 가능해집니다.

preflight request

그럼 preflight request는 무엇이고 어떤 조건에서 발생할까요?

preflight request는 브라우저의 요청이 안전하고 유효한지를 확인하기 위해 브라우저가 실제 요청을 보내기 전에 OPTIONS 메서드로 사전 요청을 보내는 개념이라고 생각하면 쉽습니다. 이런 사전 요청을 보내는 이유는 잘못되거나 위험한 요청으로부터 서버를 보호하기 위함입니다. 특히 DELETE 같이 무언가 삭제를 하는 작업에 대한 요청은 잘못 보내질 경우 굉장히 위험할 수 있기 때문입니다.

물론 모든 요청에 대해서 preflight request를 보내진 않습니다. MDN에 적혀있는 preflight request이 보내지는 조건은 다음 모든 것을 만족할 때입니다.

  • GET, HEAD, POST 요청
  • Content-Type 헤더가 아래의 값들인 경우
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 요청에 사용된 XMLHttpRequest.upload 객체에 이벤트 리스너가 등록되어 있지 않을 때
  • ReadableStream 객체가 요청에서 사용되지 않을 때

그리고 직접 세팅할 수 있는 헤더에 대한 이야기도 위의 MDN 링크로 들어가 보시면 확인하실 수 있습니다.

Content-Type에 보면 application/json은 없는 걸 확인하실 수 있는데, POST 통신 시 json으로 내용을 보내는 경우도 종종 있기에, 이런 경우는 단순 요청으로 인식되지 않아 preflight request가 보내집니다. 이 경우가 바로 제가 맨 위에서 캡쳐로 보여드렸던 케이스이기도 합니다.

결론적으로, 위의 조건을 모두 만족하는 단순 요청은 CORS에 대한 별다른 처리 없이도 요청과 응답이 잘 이루어집니다. 그래서 저도 처음엔 GET 요청을 보내 응답을 받는 것에 문제가 없었던 것이죠. GET통신이 잘 된다고 CORS를 신경 쓰지 않으면 아마 추후 다른 API가 붙을 때, 문제를 겪을 수 있으니 미리 서버 개발자와 프론트 개발자가 이야기를 할 필요가 있습니다.

Postman에서는 왜 문제가 없는 거죠?

제가 겪었던 또 하나의 시행착오라고 할 수 있는 것이 바로 Postman만을 이용한 API 테스팅입니다. 포스트맨에서 요청을 보내면 아주 시원시원하게 모든 것이 동작하기 때문에 나중에 배포하는 시점이 되어서야 CORS문제를 맞닥뜨리게 되었습니다. 이유는 단순합니다. CORS, 더 정확히는 SOP는 어디까지나 브라우저의 제한사항일 뿐, 그 외의 것들은 상관이 없습니다. 그저 다른 오리진의 웹 애플리케이션이 접근하는 것을 막기 위해 탄생한 개념으로 애초에 '오리진'이랄 게 없는 포스트맨과 같은 개발 도구에선 CORS 처리가 불필요합니다. 위에서도 설명했지만, 사실상 통신을 차단하는 것은 서버 쪽이 아니라 브라우저 쪽이기 때문입니다. 이런 비슷한 맥락으로 iOS나 안드로이드 입장에서도 서버와 API 통신을 할 때 CORS를 신경 써줄 필요가 없습니다.

CORS 이슈 해결 방법

그래서 CORS 관련 오류가 발생하면 어떻게 해야 할까요?

서버를 수정할 수 있다면

서버 쪽 코드를 수정할 수 있다면 가장 간단합니다. 서버 입장에서 요청을 보내는 브라우저의 오리진을 허용하여 헤더에 추가해주면 됩니다. Express를 이용하여 서버를 만들 때는 cors library를 이용하여 편하게 진행한 바 있습니다.

서버를 수정할 수 없다면

외부 서버와 통신하는 경우 서버를 수정할 수 있는 권한이 없습니다. 이런 경우 헤더를 수정하는 방식을 취하기 어렵기 때문에 proxy 서버를 하나 두고 그 서버가 외부 서버와 소통하게 하는 방법이 있습니다. 즉, 중계 서버를 만들어 브라우저 대신 요청을 보내고 받아와 다시 브라우저에게 전달해주는 것입니다.

이 하나를 위해 서버를 직접 만들 필요는 없고, Webpack DevServer Proxy 같은 걸 활용하여 간단하게 요청과 응답을 대신하는 프록시 서버를 활용할 수 있습니다.

이 외에 jsonp와 같은 우회 방법이 있다고 합니다. 보안 등의 이유로 크게 추천되는 방식은 아니기에 위의 두 가지를 적용해볼 수 없는 상황에만 사용을 고려해보는 것이 좋을 것 같습니다.

레퍼런스

SOP, CORS, CSRF and XSS simply explained with examples

CORS - Is it a client-side thing, a server-side thing, or a transport level thing?