본문 바로가기
홍TV 홍TV

SOP의 한계와 이를 보완하는 ‘CORS’의 심층 메커니즘

홍TV 읽는 시간 약 9분
4.5
(300)

들어가며

개발하다 보면 콘솔에 빨간 글씨로 뜨는 그 에러, 다들 한 번쯤 마주쳤을 겁니다. has been blocked by CORS policy 라는 메시지 말이죠. 대부분은 서버 쪽 헤더 하나 추가하고 넘어가지만, 왜 브라우저가 갑자기 요청을 두 번 보내는지, 왜 특정 조건에서만 그런 일이 벌어지는지 제대로 설명할 수 있는 사람은 생각보다 많지 않습니다. 이 글에서는 SOP(Same-Origin Policy)가 왜 존재하는지부터 시작해서, MSA 환경에서 필수가 된 CORS가 내부적으로 어떻게 동작하는지, 그리고 그 과정에서 놓치기 쉬운 보안 취약점까지 순서대로 짚어보겠습니다. 단순히 에러를 없애는 설정법이 아니라, 그 설정이 왜 필요하고 잘못 쓰면 무엇이 위험한지에 초점을 맞췄습니다.

SOP만으로는 충분하지 않은 이유

브라우저는 기본적으로 SOP라는 규칙을 따릅니다. 프로토콜, 도메인, 포트 세 가지가 모두 같아야 같은 출처로 인정하고, 하나라도 다르면 다른 출처의 리소스에 스크립트가 직접 접근하지 못하게 막습니다. 만약 이 규칙이 없다면 사용자가 은행 사이트에 로그인한 상태에서 악성 사이트를 열었을 때, 그 악성 스크립트가 은행 사이트의 API를 조용히 호출해서 계좌 정보를 읽어갈 수도 있습니다.

그런데 문제는 지금의 웹 서비스 구조입니다. 프론트엔드와 백엔드가 서로 다른 도메인이나 포트에서 서비스되는 경우가 흔하고, MSA 구조에서는 하나의 화면이 여러 개의 마이크로서비스 API를 동시에 호출하는 일도 많습니다. SOP를 그대로 두면 이런 정상적인 통신까지 전부 막혀버립니다. 그래서 나온 절충안이 CORS입니다. 서버가 특정 출처에 한해서 접근을 허용한다고 명시적으로 선언하면, 브라우저가 그 선언을 믿고 요청을 통과시켜주는 구조입니다.

Simple Request와 Preflighted Request를 가르는 기준

브라우저는 모든 교차 출처 요청을 똑같이 취급하지 않습니다. 크게 두 종류로 나뉘는데, 하나는 Simple Request이고 다른 하나는 Preflighted Request입니다. Simple Request로 인정받으려면 몇 가지 조건을 동시에 만족해야 합니다.

  1. 메서드가 GET, POST, HEAD 중 하나여야 합니다
  2. Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 합니다
  3. Authorization 같은 커스텀 헤더를 추가로 붙이지 않아야 합니다

이 조건을 모두 만족하면 브라우저는 예비 요청 없이 바로 실제 요청을 보내고, 응답이 돌아온 뒤에 헤더를 검사해서 스크립트에 결과를 넘길지 말지를 결정합니다. 반대로 요청 본문이 JSON이라 Content-Type이 application/json이 되거나, 로그인 토큰을 담은 커스텀 헤더를 붙이는 순간 이 요청은 Preflighted Request로 분류됩니다. 실무에서 만드는 API 요청의 대다수가 사실 이 두 번째 조건에 걸립니다. 그래서 개발자 입장에서는 거의 항상 예비 요청이 따라붙는다고 느끼는 것이고, 실제로도 그렇습니다.

이렇게 구분하는 이유는 간단합니다. Simple Request는 사실 HTML 폼 태그만으로도 예전부터 얼마든지 보낼 수 있었던 종류의 요청입니다. 즉 CORS라는 개념이 생기기 전에도 브라우저가 이미 허용하고 있던 동작이라, 굳이 새로운 검증 절차를 추가할 필요가 없었던 겁니다. 반면 커스텀 헤더나 JSON 페이로드를 사용하는 요청은 기존 웹 표준에는 없던, CORS 스펙이 새로 열어준 능력이기 때문에 브라우저가 먼저 서버의 의사를 확인하는 절차를 끼워 넣은 것입니다.

Preflight Request는 왜, 언제 나가는가

Preflighted Request로 분류되면 브라우저는 실제 요청을 보내기 전에 OPTIONS 메서드로 예비 요청을 먼저 보냅니다. 이 요청에는 Access-Control-Request-Method와 Access-Control-Request-Headers라는 헤더가 담겨서, 이런 메서드와 이런 헤더로 실제 요청을 보낼 예정인데 괜찮은지를 서버에 미리 물어보는 셈입니다. 서버는 이 질문에 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 같은 헤더로 답을 하고, 브라우저는 그 답을 보고 실제 요청을 보낼지 말지를 결정합니다. 이 과정에서 서버의 실제 비즈니스 로직은 전혀 실행되지 않습니다. 단순히 요청을 허용할지 말지를 판단하는 절차이기 때문에, 요청이 두 번 나가는 것처럼 보여도 서버 자원을 두 번 소모하는 것은 아닙니다.

다만 네트워크 왕복이 한 번 더 생기는 것은 사실이라 성능에 영향을 줍니다. 특히 API 호출이 잦은 화면에서는 이 왕복 시간이 누적되어 체감 속도가 느려질 수 있습니다. 이를 줄이는 방법이 바로 Preflight 결과 캐싱입니다. 서버가 응답 헤더에 Access-Control-Max-Age 값을 초 단위로 내려주면, 브라우저는 그 시간 동안 같은 출처, 같은 메서드, 같은 헤더 조합에 대해서는 예비 요청을 다시 보내지 않고 캐시된 허용 결과를 그대로 사용합니다. 다만 브라우저마다 이 값의 상한선이 다르게 정해져 있어서, 아무리 큰 값을 넣어도 무한정 캐싱되지는 않는다는 점은 알아둘 필요가 있습니다.

브라우저는 왜 OPTIONS를 먼저 보낼까, CORS의 숨은 원리

Access-Control-Allow-Origin: * 가 위험한 진짜 이유

CORS 에러를 가장 빠르게 없애는 방법은 서버에서 Access-Control-Allow-Origin 값을 별표 하나로 설정하는 것입니다. 실제로 검색만 해봐도 이 방법이 가장 먼저 나오고, 급하게 에러를 없애야 하는 상황에서는 유혹적인 선택지입니다. 문제는 이 설정이 말 그대로 모든 출처의 요청을 허용한다는 뜻이라는 점입니다. 내가 만든 프론트엔드뿐 아니라 이름 모를 어떤 사이트에서 스크립트로 내 API를 호출해도 브라우저는 응답을 그대로 통과시켜 줍니다. 만약 이 API가 인증 없이도 개인 데이터를 돌려주는 구조라면, 공격자는 자신의 사이트에 스크립트 몇 줄만 심어서 방문자의 브라우저를 통해 데이터를 긁어갈 수 있습니다.

다행히 브라우저는 여기에 한 가지 안전장치를 두고 있습니다. 요청에 credentials, 즉 쿠키나 인증 정보를 함께 실어 보내도록 설정한 경우에는 Access-Control-Allow-Origin에 별표를 쓸 수 없습니다. 이 값이 별표로 되어 있으면 브라우저가 아예 응답 자체를 거부합니다. 그래서 세션 쿠키 기반 인증을 쓰는 서비스라면 별표 설정만으로는 애초에 요청이 성립하지 않아 어느 정도 방어가 됩니다.

하지만 함정은 따로 있습니다. 인증 토큰을 쿠키가 아니라 Authorization 헤더에 직접 담아 보내는 구조라면 이 안전장치가 작동하지 않습니다. credentials 옵션과 무관하게 헤더에 토큰을 실어 보내는 방식이기 때문에, 별표 설정이 그대로 유효하게 동작하면서 토큰과 함께 응답이 그대로 노출될 수 있습니다. 결국 안전한 방법은 하나뿐입니다. 허용할 출처를 화이트리스트로 명시적으로 관리하고, 요청이 들어올 때마다 Origin 헤더 값을 그 목록과 비교해서 일치할 때만 해당 출처를 Access-Control-Allow-Origin에 동적으로 넣어주는 방식입니다. 번거롭더라도 이 절차를 생략하지 않는 것이 MSA 환경에서 여러 서비스를 안전하게 연결하는 기본기입니다.

Access-Control-Allow-Origin 별표, 정말 괜찮은 설정일까요

마무리

CORS는 결국 브라우저와 서버가 서로 신뢰를 확인하는 절차입니다. 브라우저는 SOP로 기본적인 방어선을 치고, 서버는 CORS 헤더로 예외적으로 믿을 수 있는 상대를 지정합니다. 이 구조를 제대로 이해하고 있으면 콘솔에 뜨는 에러 메시지를 무작정 지우는 게 아니라, 지금 내 서비스가 어떤 조건에서 예비 요청을 보내고 있는지, 그리고 그 허용 범위를 얼마나 좁게 설정해야 하는지를 스스로 판단할 수 있게 됩니다. 특히 여러 개의 마이크로서비스가 서로 API를 호출하는 구조라면, 이 기본기 하나가 서비스 전체의 보안 수준을 좌우한다고 해도 과언이 아닙니다.

참고

이 글은 CORS와 관련된 공개된 웹 표준 문서 및 개발 자료를 바탕으로 이해를 돕기 위해 정리한 내용입니다.

이 게시물이 얼마나 유용했나요?

별점을 클릭하여 평가하세요!

평균 평점 4.5 / 5. 투표 수: 300

아직 투표가 없습니다! 첫 번째로 평가해보세요.

홍TV

홍TV
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.