Cross-Origin Resource Sharing과 Preflight Request를 하는 이유에 대해서

2024. 6. 15. 15:08Network

1. 소개

요즘날, 우리가 방문하는 웹 페이지들은 자주 다른 서버들에게 우리가 보는 데이터를 보여주기 위해서 요청을 합니다. 이것은 Cross-Origin Resource Sharing(CORS)이라고 부릅니다. 이번 글에서는 CORS가 무엇이고 CORS 정책이 브라우저마다 어떻게 구현되어 있는지 알아보고, 우리가 왜 preflight request를 해야 하는지 알아봅니다.

2. Same Origin Policy는 무엇인가?

CORS에 대해서 논의하기 전에, Same Origin Policy에 대해서 소개합니다. Same Origin Policy는 하나의 origin(도메인)에서 스크립트를 이용하여 다른 origin의 리소스에 접근하거나 처리를 요구하는 것을 기본적으로 제한하고 있습니다.

Same Origin Policy에서 우리는 두 URL이 동일한 프로토콜, 도메인 및 포트번호를 가지면(URL에 포트가 포함된 경우) 동일한 발신지인 것으로 판단합니다.

 

예를 들어 다음 두 URL 주소는 Same Origin으로 판단합니다.

<https://www.baeldung.com>
<https://www.baeldung.com/abo>
  • 프로토콜 : https
  • 도메인 : www.baeldung.com
  • 포트 : 생략되었지만 https 프로토콜에서 생략된 경우 443으로 설정

 

반면에 다음 두 URL 주소는 다른 Same Origin으로 판단합니다.

<https://www.baeldung.com>
<http://www.baeldung.com>
  • 두 URL이 다른 Same Origin으로 판단한 이유는 프로토콜이 서로 다르기 때문입니다.

 

Same Origin Policy를 구현한 브라우저들은 XMLHttpRequest나 Fetch API와 같은 방법들을 사용하여 한 origin에서 다른 origin으로 리소스 또는 리소스 처리 요청들을 하는 웹 사이트 스크립트들을 제한합니다.

cross-site request forgery(크로스 사이트 요청 위조) 공격으로부터 보호하기 위한 첫번째 방법으로 Same Origin Policy를 수행합니다. 그러나 개발자들은 서로 다른 도메인의 API에서 리소스를 요청하고 다른 도메인에서 제공하는 기능들을 활용하는 것이 유용하다는 것을 알게 되었습니다. 이러한 이유로 CORS 정책은 특정한 조건 하에서 다른 도메인의 리소스 공유를 허용하는 것을 부차적으로 소개되고 제안되고 있습니다. 다음 장에서 이것을 어떻게 가능한지에 대해서 알아봅니다.

이후의 설명에서는 origin을 도메인으로 표현하여 부르도록 하겠습니다.

 

3. CORS는 무엇인가?

서드 파티 API들에 접근해야 하는 필요성을 해결하기 위해서 CORS 정책이란 한 도메인에서 제공하는 스크립트가 다른 도메인의 리소스를 요청할 수 있는 방법을 결정하는 정책입니다.

CORS 정책은 서버가 리소스 요청을 허용할 도메인들을 request/response에 포함되어야 하는 특정한 HTTP 헤더들을 정의합니다. 브라우저는 그런 다음에 response에 접근하여 스크립트 실행을 허용할 것인지 막을 것인지 결정합니다.

브라우저가 cross-origin 요청을 하는 3가지 유형은 다음과 같습니다.

  • Simple Requests
  • Non-Simple Requests
  • Credentialed Requests

 

3.1 Simple Requests

간단한 요청(simple requests)은 다음 조건을 만족하는 요청들을 의미합니다.

  • HTTP 메서드 방식이 GET, HEAD, POST인 요청
  • User-Agent 헤더만 보내거나 Accept, Accept-Languate, Content-Languate, Content-Type과 같은 CORS 세이프 목록 헤더만 보내는 요청
  • Content-Type 헤더가 오직 “application/x-www-form-urlencoded”, “multipart/form-data”, “text/plain”만을 가지고 있는 요청
  • ReadableStream object를 사용하지 않는 요청
  • XMLHttpRequest.upload에 이벤트 리스너 연결되어 있지 않은 요청

 

ReadableStream
웹 API 중 하나로, 브라우저와 자바스크립트 환경에서 읽기 가능한 스트림을 구현합니다. ReadableStream은 읽기 작업에 특화된 스트림으로 데이터 소스로부터 데이터를 청크 단위로 읽어올 수 있습니다.

 

다음 코드는 “https://www.site.com”에서 수행하는 자바스크립트에 의해서 간단한 CORS 요청하는 코드입니다.

const xhr = new XMLHttpRequest(); 
const url = '<https://www.api.com?q=test>'; 
xhr.open('GET', url); 
xhr.onreadystatechange = requestHandler; 
xhr.send();

 

위 코드를 실행하여 브라우저와 “https://www.api.com” 사이에서 다음과 같은 일이 발생합니다.

  • 첫번째로 브라우저는 “https://www.api.com” 서버에 도메인을 식별하는 origin header를 포함한 HTTP 요청을 전송합니다.
  • 서버는 요청된 데이터와 함께 응답하고 또한 리스폰스에는 access-control-allow-origin 헤더를 포함합니다. 브라우저는 access-control-allow-origin 헤더를 통해서 요청을 허용할 도메인을 브라우저에게 알려줍니다.
  • 브라우저는 requestHandler 메서드가 리스폰스 데이터에 접근할 수 있도록 허용해서 CORS를 할 수 있도록 합니다.

 

위 과정을 그림으로 표현하면 다음과 같습니다.

 

access-control-allow-origin 헤더는 서버가 요청을 허용하는 도메인들을 나타내는 CORS의 주요 헤더 중 하나입니다. access-control-allow-origin 헤더의 값은 브라우저에 특정 도메인에 대한 접근을 허용하는 단일 도메인 값이거나 브라우저에 모든 도메인을 허용하는 와일드 카드(*)일 수 있습니다.

만약 서버가 access-control-allow-origin 헤더를 응답하지 않거나 헤더의 값이 요청하는 도메인의 값과 일치하지 않는다면, 브라우저는 스크립트로 전달되는 리스폰스를 막을 것입니다. 이 에러는 콘솔에 표시될 것입니다. 예를 들어 다음 결과는 우리의 웹 사이트에서 “http://www.google.com”으로 GET 요청을 하였을때의 크롬 브라우저 에러를 보여줍니다.

 

 

3.2 Non-Simple Requests

단순한 요청(simple request)가 아닌 모든 요청은 단순하지 않은 요청(non-simple request)이거나 미리 준비된 요청(preflighted request)으로 간주됩니다. 브라우저는 이러한 종류의 요청들을 조금 다르게 취급합니다. 실제 요청을 전송하기 전에 서버가 이러한 종류의 요청을 허용할 수 있는지 확인하기 위해서 브라우저는 preflight request을 보냅니다. preflight requestsms 다음 헤더들을 포함하는 OPTIONS HTTP 요청을 전송합니다.

  • origin : 요청이 어디에서 요청되었는지 서버에게 알려주기 위한 도메인 값
  • access-control-request-method : 요청하는 HTTP의 Http Method를 서버에 알려줍니다.
  • access-control-request-headers : HTTP 요청에 포함되는 헤더들을 서버에 알려줍니다.

 

preflight request 요청하였을때 서버는 다음 헤더들을 응답해서 브라우저는 요청한 도메인(origin)에서 이러한 종류의 요청을 수락할지 여부를 결정할 수 있습니다.

  • access-control-allow-origin : 서버가 허용하는 도메인
  • access-control-allow-methods : 서버가 허용하는 HTTP Method 목록
    • HTTP Method 목록은 콤마로 구분됩니다.
  • access-control-allow-headers : 서버가 허용하는 HTTP Header 목록
    • HTTP Header 목록은 콤마로 구분됩니다.
  • aceess-control-max-age : 브라우저에게 prelight request에 대한 응답을 어마나 길게(초 단위) 캐시할 것인지 알려줍니다.
  • 가능한 CORS 응답 헤더의 전체 목록은 MDN 웹 문서에 있습니다.

 

간단한 요청과 비슷하게 만약 서버가 몇몇 위의 CORS 헤더들을 포함하지 않는다면, 브라우저는 서버가 이 요청을 허용하지 않는다고 판단하고 실제 요청을 진행하지 않습니다.

이전 요청을 비슷하게 수정해서 다음과 같은 실행을 해봅니다. custom-header를 추가해서 non-simple request를 수행합니다.

const xhr = new XMLHttpRequest();
const url = '<https://www.api.com?q=test>';
xhr.open(‘GET', url);
xhr.setRequestHeader(‘custom-header', ’test')
xhr.onreadystatechange = requestHandler;
xhr.send();

 

custom-header 헤더를 추가했기 때문에 브라우저는 non-simple request로 식별하고 서버에 preflight request를 우선 전송합니다. 다음 그림은 “https://www.api.com” 서버가 이러한 종류의 요청을 허용했을때의 수행 과정을 나타냅니다.

 

 

 

위 그림을 보면 서버는 Pre-flight 요청에 대한 응답으로 정확한 헤더를 응답했고, 브라우저는 실제 요청을 진행합니다. 만약 서버가 필수적인 헤더 없이 응답했다면, 브라우저는 요청을 진행하지 않을 것입니다.

다음 결과는 custom header를 포함하는 non-simple 요청을 Google Book API에 접근했을때의 결과입니다. 우리는 브라우저 콘솔에서 조금 다른 에러를 볼 수 있을 것입니다. 왜냐하면 API는 preflight request에 대한 응답으로 필요한 헤더들을 포함하지 않았기 때문입니다. 즉, 구글 API 서버는 preflight request에 대한 응답으로 Access-Control-Allow-Origin 헤더를 포함하지 않고 응답하여 에러가 발생한 것입니다.

 

 

 

3.3 Credentialed Requests

Credentials란 의미는 자격 정보라는 의미로써 이 정보는 쿠키(cookies)가 될수도 있고, authorization 헤더가 될수도 있고, TLS client certificate(TLS 클라이언트 인증서)가 될 수 있습니다. 기본적으로 CORS 정책은 credential을 포함할 플래그가 포함되어 있고, 서버가 access-control-allow-credentials=true로 응답하지 않는 한 cross-origin request에서 credential 정보가 포함되는 것을 허용하지 않습니다.

TLS(Transport Layer Secure)
TLS는 인터넷 통신의 보안을 담당하는 프로토콜입니다. 주로 데이터를 암호화하고 인증서를 통해서 통신 당사자들을 확인하여 무결성과 기밀성을 보장합니다.

 

우리의 요청에서 credential 정보를 포함하기 위해서 withCredentials 프로퍼티를 true로 설정합니다.

const xhr = new XMLHttpRequest();
const url = '<https://www.api.com?q=test>';
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();

 

만약 서버가 access-control-allow-origin 헤더의 도메인이 요청하고자 하는 도메인과 같지만 access-control-allow-credentials 헤더의 값이 true로 포함되어 있지 않으면, 브라우저는 요청을 막습니다.

이 예제에서는 crednetial 정보를 허용하지 않는 Google Book API에 같은 요청을 했을때 다음과 같은 결과를 볼 수 있습니다.

 

 

4. 왜 Preflight Request를 하는가?

Same Origin Policy가 시행된지 얼마 되지 않아 CORS(Cross-Origin Resource Sharing)가 제안되었기 때문에, preflight request을 사용하는 아이디어는 몇가지 장점을 제공했습니다. preflight request를 통해 서버는 요청을 실행하기 전에 요청을 검사하고 허용 여부를 응답할 수 있습니다.

특정한 요청이 다른 도메인에서 발생하여 서버에서 허용하지 않는 부작용을 일으키는 경우, preflight request는 서버를 보호하는 역할을 합니다. preflight request는 먼저 요청을 확인하고, 서버가 거부를 나타내는 헤더를 응답으로 보내면 요청을 차단합니다. 이와 더불어 서버는 개발 과정에서 허용하는 요청 유형과 헤더를 변경할 수 있습니다. preflight request가 있으면, 브라우저는 이를 확인하고 적절히 조정할 수 있습니다.

마지막으로, CORS는 하위 호환성을 제공합니다. Same Origin Policy에 의존하고 CORS를 처리하지 않는 일부 오래된 서버도 여전히 preflight request를 통해 보호를 받습니다. 왜냐하면 브라우저는 CORS 헤더를 보내지 않는 서버를 동일한 도메인에서만 요청을 허용하는 서버처럼 취급하기 때문에 서버는 별도의 변경 없이 CORS 요청을 거부할 수 있습니다.

CORS가 하위 호환성을 제공한다는 의미

CORS가 도입되기 전에 존재하던 웹서버와 클라이언트의 기존 동작 방식을 크게 변경하지 않으면서 새로운 기능을 추가할 수 있도록 설계되었다는 의미입니다.

 

1. 기존 웹서버와의 호환성 유지

  • 기존 서버의 무변경 동작 : CORS가 도입되기 전에 설계된 기존 서버는 변경할 필요가 없습니다. CORS를 지원하지 않는 서버는 cross-origin 요청에 대한 처리를 하지 않아도 됩니다. 이 경우에 브라우저는 Same Origin Policy에 따라 서버와의 cross-origin 요청을 차단합니다. 따라서 기존 서버는 Same Origin Policy 하에서 계속 안전하게 동작할 수 있습니다.
  • 선택적 CORS 지원 : CORS를 적용하고자 하는 서버는 CORS 정책을 명시적으로 설정해서 cross-origin 요청을 허용할 수 있습니다. 이를 통해 서버는 필요에 따라 CORS 헤더를 추가하여 cross-origin request를 허용하거나 거부할 수 있습니다. CORS를 지원하지 않는 서버는 이러한 헤더를 추가할 필요가 없고, 기존 방식대로 동작합니다.

 

2. 기존 클라이언트와의 호환성 유지

  • 구현 브라우저 지원 : CORS가 도입되기 전에 존재하던 구형 브라우저는 CORS 정책을 이해하거나 처리하지 않습니다. 이러한 구형 브라우저는 여전히 Same Origin Policy에 의하여 cross-origin 요청을 차단할 것입니다. 즉, 구형 브라우저는 CORS를 지원하지 않지만, 기존 보안 정책을 준수하여 안전하게 동작할 것입니다.
  • 단순 요청 처리 : 단순 요청(Simple Requests)은 Preflight Request 없이도 SOP에 따라 안전하게 처리됩니다. CORS가 도입된 이후에도 단순 요청은 여전히 기존의 SOP를 기반으로 처리될 수 있습니다.

3. 새로운 기능의 안전한 추가

  • Preflight Request: 복잡한 요청(Complex Requests)에서 Preflight Request를 사용하여 서버가 허용 여부를 명확히 할 수 있습니다. 이는 기존의 동작을 방해하지 않고, 추가적인 보안을 제공합니다. 서버는 Preflight Request를 통해 요청을 허용할지 여부를 결정하고, 이는 기존 요청 흐름에 영향을 주지 않습니다.
  • 헤더 기반 제어: 서버는 CORS 정책을 명시적으로 설정하여 교차 출처 요청을 제어할 수 있습니다. 서버가 새로운 CORS 헤더를 사용하여 허용 정책을 정의하면, 브라우저는 이러한 정책을 읽어 요청을 허용하거나 차단합니다. 이는 기존의 서버 동작 방식을 변경하지 않으면서 추가적인 보안 정책을 적용할 수 있는 방식입니다.

5. 결론

이번장에서는 cross-origin-resource policy의 몇몇 자세한 내용과 브라우저가 어떻게 CORS를 구현할 수 있는지 살펴봤습니다. CORS를 구현한 브라워즈들은 3가지 주요 타입으로 요청을 간주합니다. (simple request, preflighted request, 간단하거나 preflight 요청할 수 있는 credentialed request)

 

정리

  • CORS(Cross-Origin Resource Sharing)은 한 도메인에서 다른 도메인으로 리소스나 리소스 처리를 요청하는 방법입니다.
  • Same Origin Policy는 한 도메인에서 스크립트를 이용하여 다른 도메인으로 리소스에 접근하거나 처리를 요청하는 것을 막는 정책합니다.
  • Same Origin Policy에서 두 URL이 같은 프로토콜, 도메인, 포트번호(포트번호가 포함된 경우)를 가지면 Same Origin으로 취급합니다.
  • Same Origin Policy는 cross-site request forgery(크로스 사이트 요청 위조)를 막기 위한 첫번째 방법입니다.
  • CORS 정책이란 한 도메인에서 제공하는 스크립트가 다른 도메인의 리소스를 요청할 수 있는 방법을 결정하는 정책입니다.
  • access-control-allow-origin 헤더는 서버가 CORS 요청을 허용하는 도메인을 표시하는 헤더입니다.
  • 브라우저는 Non-Simple Requests를 전송시 먼저 preflight request를 전송합니다. preflight request는 서버가 이 요청을 허용할 수 있는지 확인하기 위해서 보내는 사전 요청입니다.
  • preflight request를 함으로써 요청에 동의하지 않은 서버들을 보호할 수 있습니다.
  • CORS가 하위 호환성을 제공한다는 의미는 CORS를 지원하지 않는 웹서버와 클라이언트의 동작을 변경하지 않으면서 새로운 기능을 추가할 수 있도록 해준다는 의미입니다.

 

References