동일 출처 정책과 CORS, 웹 보안의 기본을 쉽게 풀어보기

왜 접근 제한이 필요할까?

웹은 다양한 리소스를 주고받는 구조로 이루어져 있다. 사용자가 접속한 웹사이트에서 실행되는 자바스크립트는 브라우저를 통해 다양한 데이터를 다룰 수 있다. 그러나 이러한 구조는 사용자 데이터의 안전성을 위협하는 요소가 될 수 있다. 예를 들어, 악성 사이트에서 사용자의 쿠키를 가로채거나 다른 사이트의 API에 무단으로 요청을 보내는 공격이 가능하다. 이러한 보안 문제를 방지하기 위해 브라우저는 “접근 제한”이라는 개념을 도입했다. 이 접근 제한은 웹의 기본 동작 방식을 지키면서 보안과 개인정보 보호를 위해 꼭 필요한 원칙이다.

접근 제한은 단순히 ‘웹 페이지’라는 관점에서만 이루어지는 것이 아니라, 각 리소스의 ‘출처’를 기준으로 결정된다. 따라서 동일 출처 정책을 이해하기 위해서는 출처란 무엇인지 먼저 알아볼 필요가 있다.

출처(Origin)란?

출처란 웹에서 리소스를 구분하는 기준으로 프로토콜(scheme), 호스트(host), 포트(port) 의 조합으로 정의된다.

예를 들어 다음과 같은 URL이 있을 때,
https://example.com:443

  • 프로토콜: https
  • 호스트: example.com
  • 포트: 443

이 세 요소가 모두 같아야 동일 출처로 인식된다.

https://example.com:443https://example.com:8443은 포트 번호가 다르므로 서로 다른 출처이다.

출처의 일치 여부는 보안 정책을 적용할 때 중요한 기준이 된다. 브라우저는 서로 다른 출처 간의 무분별한 접근을 제한하여 사용자의 정보를 보호하기 위해 동일 출처 정책(Same-Origin Policy)을 적용한다.

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

웹의 리소스는 다양한 출처에서 로드될 수 있다. 하지만 이러한 구조는 보안상 문제를 일으킬 가능성이 크기 때문에, 브라우저는 동일 출처 정책(Same-Origin Policy) 이라는 보안 메커니즘을 적용한다. 동일 출처 정책이란, 한 출처에서 로드된 스크립트가 다른 출처의 리소스에 접근하는 것을 제한하는 정책이다.

쉽게 말해, 웹 페이지에서 불러온 자바스크립트 코드가 자신이 로드된 출처의 리소스만 접근할 수 있도록 제한하는 것이다. 이를 통해 악성 코드가 다른 출처의 민감한 정보에 접근하지 못하도록 막는다.

동일 출처 정책이 왜 필요할까?

동일 출처 정책은 웹 보안을 위해 꼭 필요하다. 만약 동일 출처 정책이 없다면 악성 코드가 사용자의 세션 쿠키, 로컬 스토리지 또는 다른 출처의 데이터를 훔칠 수 있다. 이를 통해 개인 정보 탈취, 세션 하이재킹, 계정 도용 등 심각한 보안 문제가 발생할 수 있다.

즉, 동일 출처 정책은 웹 애플리케이션의 무결성과 사용자 정보를 보호하기 위한 최소한의 보안 장치이다.

동일 출처 정책은 언제 적용될까?

동일 출처 정책은 다양한 상황에서 적용된다. 대표적인 예시는 다음과 같다.

  • 다른 출처의 웹 페이지를 <iframe>에 삽입할 경우, 삽입된 페이지의 DOM에 접근할 수 없다.
  • 자바스크립트로 다른 출처의 API에 fetchXMLHttpRequest 요청을 보낼 수는 있지만, 응답 데이터에 접근하려고 하면 브라우저에서 차단된다.
  • 로컬 스토리지(LocalStorage)와 세션 스토리지(SessionStorage)는 출처 단위로 격리되며, 다른 출처의 데이터에 접근할 수 없다.

이러한 제한을 통해 브라우저는 클라이언트 측에서의 무분별한 접근을 차단하여 보안을 강화한다.

동일 출처 정책의 예외 (허용은 되지만 제한된 경우)

모든 요청이 동일 출처 정책의 제한을 받는 것은 아니다.
예를 들어, <img>, <script>, <link>, <video>와 같은 정적 리소스는 다른 출처라도 요청이 가능하다. 이러한 리소스들은 단순히 표시하거나 실행만 할 뿐, 자바스크립트로 직접 접근하거나 데이터를 읽을 수는 없다.

그러나 보안상 이유로 특정 리소스에 대한 접근을 더 엄격하게 제한해야 할 경우에는 서버에서 CORS를 설정하거나, HTML 태그에서 crossorigin 속성을 지정하여 교차 출처 요청의 범위를 제어할 수 있다.

CORS(Cross-Origin Resource Sharing)란?

CORS(Cross-Origin Resource Sharing) 는 교차 출처 요청을 안전하게 허용하기 위한 표준이다. 동일 출처 정책에 의해 차단되는 요청을 서버가 명시적으로 허용할 수 있도록 하는 방법이다.

서버는 응답 헤더에 Access-Control-Allow-Origin과 같은 CORS 관련 헤더를 포함하여 특정 출처의 요청을 허용하거나 거부한다. 즉, CORS는 브라우저가 요청과 응답을 처리할 때 서버의 허용 여부를 확인하는 기준이 된다.

하지만 CORS가 단순히 ‘허용’ 여부만 판단하는 것은 아니다. 브라우저는 요청의 성격과 위험성을 고려해 요청을 다르게 처리하며, 요청을 단순 요청과 사전 요청이 필요한 요청으로 구분한다. 요청의 방식과 포함된 헤더, 메서드 등에 따라 어떤 요청이 필요한지 판단하게 된다. 이 구분은 CORS 정책의 핵심적인 동작 방식 중 하나이다.

단순 요청(Simple Request)이란 무엇인가?

단순 요청이란, 브라우저가 보안상 위험하지 않다고 판단하여 사전 요청(Preflight Request) 없이 바로 서버에 전송하는 요청을 의미한다. 단순 요청은 브라우저와 서버 간의 추가 협의 없이 곧바로 처리되며, 이를 통해 응답 속도를 빠르게 하고, 복잡한 확인 절차를 줄일 수 있다.

단순 요청으로 인정되기 위해서는 다음과 같은 조건을 모두 만족해야 한다.

  • HTTP 메서드GET, POST, HEAD 중 하나여야 한다.
  • Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 한다.
  • 요청 헤더에는 사용자 정의 헤더(custom header) 가 포함되면 안 된다. (예, Authorization, X-Custom-Header 등의 헤더는 단순 요청을 벗어나게 한다.)

이러한 조건들은 브라우저가 위험도가 낮다고 판단하는 요청의 범위를 지정한 것으로 서버의 추가 확인 없이 처리 가능한 요청을 구분하기 위함이다.

예를 들어, 다음과 같은 요청은 단순 요청에 해당한다.

fetch('https://api.example.com/data', {
	method: 'GET',
});

반면, 다음과 같이 PUT 메서드를 사용하거나 Authorization 헤더를 포함하면 단순 요청이 아니며, Preflight Request가 발생한다.

fetch('https://api.example.com/data', {
	method: 'PUT',
	headers: {
		Authorization: 'Bearer token',
	},
});

사전 요청(Preflight Request)란 무엇이며, 왜 필요할까?

단순 요청과 달리, 보안상 위험도가 높다고 판단되는 요청은 브라우저가 먼저 서버의 허락을 받기 위해 사전 요청(Preflight Request)을 보낸다. Preflight Request는 브라우저가 실제 요청 전에 OPTIONS 메서드로 서버에 요청을 보내, “이런 요청을 보내도 괜찮나요?” 라고 확인하는 단계이다.

예를 들어, PUT, DELETE와 같은 메서드를 사용하거나 Authorization, Content-Type: application/json과 같은 커스텀 헤더를 포함하는 경우, 브라우저는 이를 위험도가 높은 요청으로 인식하고 Preflight Request를 수행한다.

서버는 Preflight 요청에 대해 다음과 같은 헤더를 응답해야 한다.

  • Access-Control-Allow-Methods: 허용하는 HTTP 메서드
  • Access-Control-Allow-Headers: 허용하는 헤더

브라우저는 이 응답을 보고 요청을 허용할지 판단하며 허용되면 실제 요청을 이어서 보낸다.

Preflight Request는 단순히 메서드와 헤더뿐 아니라, 쿠키(자격 증명 포함 여부)도 함께 검토하는 중요한 과정이다.

쿠키를 포함한 요청은 어떻게 해야 할까?

CORS 요청은 기본적으로 쿠키와 같은 자격 증명을 포함하지 않는다. 하지만, 인증이 필요한 요청에서는 쿠키를 포함해야 하는 경우가 많다. 이때는 클라이언트와 서버 모두에서 명시적인 설정이 필요하다.

  • 클라이언트: fetch 또는 axios 요청에 credentials: 'include' 또는 withCredentials: true를 설정한다.
  • 서버: Access-Control-Allow-Origin에 특정 출처를 명시하고, Access-Control-Allow-Credentials: true를 반드시 포함한다.

이 두 가지 설정이 모두 충족되어야만 쿠키를 포함한 요청이 가능하며 브라우저는 이를 통해 보안과 데이터 전송의 안전성을 보장한다.

클라이언트에서 CORS 요청 모드는 어떻게 지정할까?

CORS 요청 모드는 클라이언트 자바스크립트에서 CORS 동작 방식을 제어하는 방법이다. fetch API의 mode 옵션을 통해 요청 모드를 지정할 수 있으며 주요 모드는 다음과 같다.

  • cors: 교차 출처 요청을 허용한다. (기본 모드)
  • same-origin: 동일 출처 요청만 허용한다.
  • no-cors: 교차 출처 요청은 가능하지만, 응답 데이터를 읽을 수 없다.

브라우저는 mode 설정과 서버의 CORS 정책을 함께 고려하여 요청을 허용하거나 차단한다. 따라서 클라이언트 코드에서도 적절한 모드를 설정하고 서버 정책과 일치하도록 맞추는 것이 중요하다.

CORS 적용 사례와 오류 상황

CORS 정책은 교차 출처 요청에서 브라우저가 보안상의 이유로 차단할 수 있는 상황에 적용된다. 대표적인 사례는 다음과 같다.

  • 프론트엔드에서 외부 API에 데이터를 요청할 때, 서버가 Access-Control-Allow-Origin 헤더를 응답에 포함하지 않으면 브라우저는 응답 데이터를 읽을 수 없도록 차단한다.

  • CDN에서 제공하는 외부 스크립트나 폰트 파일을 불러올 때, crossorigin 속성을 설정하지 않으면 일부 브라우저에서 에러가 발생하거나 자격 증명을 요구하는 리소스에 접근할 수 없다.

이러한 사례는 CORS 정책과 crossorigin 설정의 필요성을 잘 보여준다.

HTML 태그에서 crossorigin 속성은 왜 필요할까?

crossorigin 속성은 HTML 태그에서 CORS 요청의 동작을 제어하기 위해 사용된다. <img>, <script>, <link> 등의 태그에서 사용되며 주요 값은 다음과 같다.

  • anonymous: 자격 증명 없이 요청을 보낸다.
  • use-credentials: 쿠키를 포함한 자격 증명을 함께 요청한다.

예를 들어, 외부 서버에서 제공하는 스크립트를 불러올 때는 다음과 같이 사용할 수 있다.

<script src="https://cdn.example.com/library.js" crossorigin="anonymous"></script>

이렇게 crossorigin 속성을 설정하면 외부 리소스의 CORS 요청 동작을 명시적으로 관리할 수 있다. 브라우저는 crossorigin 속성의 값에 따라 요청에 쿠키를 포함할지 여부를 판단한다.

이런 crossorigin 속성은 서버의 CORS 설정과 함께 동작해야 효과를 발휘한다. 즉, 서버에서 CORS를 허용하지 않으면, crossorigin 속성이 있어도 교차 출처 요청은 차단된다.

CORS 설정 시 주의해야 할 점

CORS는 필요한 요청을 허용하는 동시에 잘못된 설정은 보안 취약점을 유발할 수 있다. 예를 들어, 서버에서 Access-Control-Allow-Origin: *를 설정하면 모든 출처의 요청을 허용하게 되는데, 이는 민감한 데이터가 포함된 API나 리소스에 접근할 수 있는 허점을 만든다.

특히, Access-Control-Allow-Credentials: true와 함께 *를 사용하는 것은 명세상 허용되지 않으며, 이를 잘못 설정하면 공격자가 사용자의 쿠키를 탈취하거나 세션을 가로챌 위험이 있다. 따라서 CORS를 설정할 때는 반드시 필요한 출처만을 허용해야 하며 보안과 편리함 사이의 균형을 고려해야 한다.

마무리 말

해당 부분을 공부하며 지난 “행동대장” 프로젝트에서 CORS 에러를 마주치고 쿠키를 포함시켜 요청을 보내기 위해 많은 시간과 노력을 들였던 경험이 떠올랐다. 그때, 이 내용을 알았더라면 좀 더 빠르게 문제를 해결 할 수 있었을 것이라는 아쉬움이 들었다. 그러나, 이런 경험이 있기 때문에 이번에 해당 파트를 공부하면서 이해하기가 수월했던 것 같다. 다음에 비슷한 상황을 마주치게 된다면 공부했던 내용과 겪었던 경험을 떠올리며 이전보다는 수월하게 문제를 해결 할 수 있는 개발자가 되기를..!

📚 참고

Comments