여기서는 신입 개발자들이 프로젝트를할 때 가장 많이 접하는 CORS(Cross-Origin Resource Sharing)에 대해서 다룰 것입니다.

먼저 CORS에서 이해하기 위해서는 SOP(Same-Origin Policy)에 대해서 이해해야 합니다.

SOP

SOP(동일 출처 정책)은 웹 보안의 핵심적인 개념 중 하나로, 웹 브라우저가 스크립트에서 실행되는 동안 특정 출처(origin)에서 받은 문서나 스트립트가 다른 출처의 리소스와 상호 작용 하는 것을 제한하는 보안 매커니즘입니다.

⚡️ 출처(Origin)의 정의

웹에서 출처는 다음 세 가지 요소로 정의됩니다.

  1. 프로토콜(Scheme)
  2. 도메인(Host)
  3. 포트(Port)

이 세 요소가 모두 일치해야 동일한 출처로 간주됩니다.

SOP의 주요 목적을 정리하자면 다음과 같습니다.

목적

  • 데이터 접근과 조작 제한: 웹 브라우저는 스크립트가 동일한 출처의 데이터에만 접근하고 조작할 수 있도록 제한하여 사용자의 데이터를 보호합니다.
  • 보안 위협 방지: 크로스 사이트 스크립트(XSS)과 같은 보안 위협으로부터 사용자를 보호하기 위해 설계되었습니다.

상황 설명과 함께 이해를 해봅시다.
리소스를 웹 서버인 www.example.com:80으로부터 받아왔다고 가정해봅시다.

별도의 데이터의 요청을 보내려면 스크립트가 동작을 해야 합니다. 그것은 특정 리소스에서 동작할 것이고, 이후 브라우저는 그것을 받아 목적지로 요청을 보냅니다.

데이터를 받아오고 나면 브라우저는 해당 응답의 출처를 확인합니다.

만약, 이것이 www.example.com:80가 아니라면 브라우저는 데이터를 버려버리고 CORS Error를 발생시킵니다.

즉, 이것은 브라우저 수준에서 발생하는 보안 기능이라는 것을 알아둬야합니다.

여기서, 의문점이 해소되지 않는 부분이 있는데 SOP가 XSS를 막기 위한 기술이라는 설명이 적절한지는 잘 모르겠습니다.

CORS

CORS는 다른 출처로부터의 요청에 대한 리소스 접근을 할 수 있도록 허용해주는 정책입니다.

요청 방식에 따라 다른 CORS 발생 여부

  1. <img>, <video>, <script>, <link> 태그 등

해당 방식들은 기본적으로 Cross-Origin 정책을 지원합니다.

  • <link> 태그의 href 에서 다른 사이트의 .css 리소스에 접근하는 것이 가능
  • <img> 태그의 src 에서 다른 사이트의 .png, .jpg 등의 리소스에 접근하는 것이 가능
  • <script> 태그의 src 에서 다른 사이트의 .js 리소스에 접근하는 것이 가능(type="module" 속성은 제외)
  1. XMLHttpRequest, Fetch API 스크립트

해당 방식들은 기본적으로 Same-Origin 정책을 따릅니다.

  • 다른 도메인 소스에 대해 자바스크립트 ajax 요청 API 호출시
  • 웹 폰트 CSS 파일 내 @font-face에서 다른 도메인 폰트 사용 시

자바스크립트에서의 요청은 기본적으로 서로 다른 도메인에 대한 요청을 보안상 제한합니다. 브라우저는 기본적으로 하나의 서버 연결만 허용되도록 설정되어 있기 때문입니다.

왜 필요한가?

앞서 언급했듯이, SOP 정책에 맞지 않는 응답의 데이터는 브라우저 수준에서 차단을 합니다.

해당 그림에서도 볼 수 있듯이, 요청까지는 원할하게 동작을 합니다.

하지만, 허용되지 않은 출처로부터 받아오는 응답은 브라우저가 차단하면서 에러 메시지를 발생시키는 것입니다.

몇 가지 의문점이 생길만한 것들을 정리하고 가자면,

❓ 그렇다면 다른 출처에 대한 요청이 보내지면 쿠키에 담겨있는 토큰 혹은 세션 ID와 같은 것들이 노출될 수 있지 않는가?

이 의문에 답변은 "아니다"입니다. 쿠키는 기본적으로 생성된 출처로 보내는 요청에 한해서만 담깁니다. 그러나, CORS 정책에서 Access-Control-Allow-Credentials 헤더를 사용하고 프론트엔드 요청에서 credentials 옵션을 include로 설정하는 경우, 쿠키를 포함하여 다른 출처로 요청을 보낼 수 있습니다.

❓ 그렇다면 SOP 정책을 비활성화하면 CORS 문제를 피할 수 있겠네?

웹 브라우저에서 SOP 정책을 비활성화하는 것은 매우 위험합니다. 이는 악성 사이트가 사용자의 데이터를 탈취하거나 사용자를 대신하여 악의적인 행동을 할 수 있게 만듭니다. SOP는 기본적으로 웹 브라우저의 보안 모델의 일부이며, 이를 비활성화하는 것은 권장되지 않습니다.

❓ CORS 정책을 피할 수 있는 쉬운 방법은 없나?

가장 기본적으로 할 수 있는 것은 프록시(Proxy) 서버를 사용하는 것입니다.
React에서 pacakge.json을 사용해서 백엔드 서버 주소로 설정해놓고 동작을 시키면 해당 요청은 브라우저 -> 웹 서버(React 개발 서버) -> 백엔드 서버로 보내지고 응답은 역순으로 동작하기 때문에 문제없이 동작하는 것입니다.
해당 상황에서 프록시 역할을 하는 것은 React 개발 서버로 생각할 수 있습니다.

그러면 그냥 죄다 차단해버리는 것은 어떠한가? 즉, 리소스를 보내준 웹 서버에게만 요청을 보내면 해결될 것 아닌가?

하지만 인터넷은 여러 사람들에게 오픈된 환경이고, 이런 환경에서 웹페이지에서 다른 출처에 있는 리소스를 가져와 사용하는 일은 매우 흔한 일이라 무턱대고 막을 수 없는 일입니다.

그래서 몇 가지 예외 조항을 두고 다른 출처의 리소스 요청이라도 이 조항에 해당할 경우에는 허용하기로 했는데, 그 중 하나가 바로 CORS 정책을 지킨 리소스 요청입니다.

CORS

CORS(Cross-Origin Resource Sharing)은 웹 보안의 중요한 개념으로, 웹 브라우저에서 실행되는 스크립트가 다른 출처의 리소스에 접근할 수 있도록 허용하는 매커니즘입니다.

이것은 SOP의 완화 정책으로 생각할 수 있습니다.

결국 웹개발자가 자주 접하던 에러 메시지는 사실 브라우저의 SOP 정책에 따라 다른 출처의 리소스를 차단하면서 발생한 에러이며, CORS는 다른 출처의 리소스를 얻기 위한 해결 방안이였던 것입니다.

정리하자면, SOP 정책을 위반해도 CORS 정책을 따르면 다른 출처의 리소스라도 허용한다는 것입니다.

이를 이해하기 위해서는 브라우저의 CORS 기본 동작을 확인해야 합니다.

브라우저의 CORS 기본 동작

  1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달
  • 기본적으로 웹은 HTTP 프로토콜을 이용하여 서버에 요청을 보내게 되는데,
  • 이때 브라우저는 요청 헤더에 Origin 이라는 필드에 출처를 함께 담아 보냅니다.

  1. 서버는 응답헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달합니다.
  • 이후 서버가 이 요청에 대한 응답을 할 떄 응답 헤더에 Access-Control-Allow-Origin이라는 필드를 추가하고 값으로 리소스 접근이 허용된 출처 url을 내려 보냅니다.

  1. 클라이언트에서 Origin과 서버가 보내준 Access-Origin-Allow-Origin을 비교합니다.
  • 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Allow-Control-Allow-Origin을 비교해본 후 차단할지 말지를 결정합니다.
  • 만약 유효하지 않다면 이후 그 응답을 사용하지 않고 버립니다.(CORS 에러)
  • 위의 경우에는 둘 다 http://localhost:3000이기 때문에 유효하니 다른 출처의 리소스를 문제없이 가져오게 됩니다.

여기서 보면, 서버에서 리소스 접근에 대한 허용을 해주는 것을 확인할 수 있습니다.

결론적으로 CORS 문제가 발생했다면, 서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트 응답하면 되는 것입니다.

CORS 작동 방식

바로 위에서 살펴본 CORS 동작 흐름은 이해하기 쉽게 하기 위해 기본적인 작동 흐름을 보여준 것이고, 실제로는 CORS가 동작하는 방식은 한 가지가 아니라 세 가지 시나리오에 따라 변경됩니다.

Perflight Request

브라우저는 요청을 보낼 때 한번에 바로 보내지 않고, 먼저 예비요청을 보내 서버와 잘 통신되는지 확인한 후 본 요청을 보냅니다.

즉, 예비요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 안전한 요청인지 미리 확인하는 것입니다.

이때 브라우저가 예비 요청을 보내는 것을 Preflight라고 부르며, 이 예비요청의 HTTP 메서드는 OPTIONS라는 요청이 사용됩니다.

예를 들어 봅시다.

자바 스크립트로 API 요청을 보낸다고 가정해 봅시다.

  1. 자바스크립트의 fetch() 메서드를 통해 리소스를 받아오려고 합니다.
  2. 브라우저는 서버로 HTTP OPTIONS 메소드로 Preflight를 먼저 보냅니다.
    1. Origin 헤더에 자신의 출처를 넣습니다.
    2. Access-Control-Request-Method 헤더에 실제 요청을 사용할 메서드를 설정합니다.
    3. Access-Control-Request-Headers 헤더에 실제 사용할 헤더를 설정합니다.
  3. 서버는 이 요청에 대한 응답으로 어떤 것을 허용하고 어떤 것을 금지하고 있는지에 대한 덩보를 헤더에 담아서 브라우저로 보냅니다.
    1. Access-Control-Allow-Origin 헤더에 허용되는 Origin들의 목록을 설정합니다.
    2. Access-Control-Allow-Methos 헤더에 허용되는 메서드들의 목록을 설정합니다.
    3. Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록을 설정합니다.
    4. Access-Control-Max-Age 헤더에 해당 예비 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정합니다.
  4. 이후 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여, 해당 요청이 안전한지 확인하고 본 요청을 보내게 됩니다.
  5. 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스크립트로 넘겨줍니다.

여기서, 만약 요청이 Preflight의 응답 헤더들의 목록들에 포함되어 있지 않는 경우 즉, 일치 하지 않는 경우 요청 자체를 차단하고 CORS 관련 오류 메시지를 발생시킵니다.

해당 과정은 안전하지 않는 요청을 동작하지 않도록 수행한다는 점에서 굉장히 효과적일 수 있어 보입니다. 하지만, 문제는 대부분의 요청들은 안전한 경우가 많을 것인데 매번 해당 과정을 수행한다면 애플리케이션 성능에 영향을 줄 것입니다.

따라서 캐싱하는 방식을 사용합니다.

브라우저 캐시(Cache)를 이용해 Access-Control-Max-Age 헤더에 캐시될 시간을 명시해 주면, 이 Preflight 요청을 캐싱 시켜 최적화 시켜줄 수 있습니다.

예비 요청 캐시는 다른 캐싱 매커니즘과 유사하게 동작합니다.

  1. 브라우저는 Preflight 요청을 할 때마다, 먼저 Preflight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인합니다.
  2. 만일 응답이 캐싱되어 있지 않다면, 서버에 예비 요청을 보내 인증 절차를 밟습니다.
  3. 만일 서버로부터 Access-Control-Max-Age 응답 헤더를 받는다면 그 기간동안 브라우저 캐시에 결과를 저장합니다.
  4. 다시 요청을 보내고 만일 응답이 캐싱되어 있다면, 예비 요청을 서버로 보내지 않고 대신 캐싱된 응답을 사용합니다.

Simple Request

단순 요청은 Preflight 동작을 수행하지 않고, 일단 요청을 보내고 응답과 함께 돌아온 Access-Control-Allow-Origin 헤더를 확인하여 브라우저가 CORS 정책 위반 여부를 검사하는 방식입니다.

다만, 심플한 만큼 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있습니다.

  1. 요청의 메서드는 GET, HEAD, POST 중 하나여야만 합니다.
  2. AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth 헤더일 경우 에만 적용된다.
  3. Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain중 하나여야한다. 아닐 경우 예비 요청으로 동작된다.

이처럼 다소 까다로운 조건들이 많기 때문에, 위 조건들이 모두 만족하여 단순 요청이 일어나는 상황은 드물다고 볼 수 있다고 합니다.

왜냐하면 HTTP API 요청은 text/xml 이나 application/json으로 통신하기 때문에 3번째 Content-Type이 위반되기 때문입니다.

따라서 대부분의 API 요청은 그냥 Preflight로 이루어진다고 이해하면 됩니다.

Credentialed Request

인증된 요청은 클라이언트에서 서버에게 자격 인증 정보(Credential)을 실어 요청할 때 사용되는 요청입니다.

여기서 말하는 자격 인증 정보란 세션 ID가 저장되어 있는 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 Token 값 등을 의미합니다.

즉, 클라이언트에게 일반적인 JSON 데이터 외에도 쿠키와 같은 인증 정보를 포함해서 다른 출처의 서버로 전달할 때 CORS의 세가지 요청 중 하나인 인증된 요청으로 동작된다는 말이며, 이는 기존의 단순 요청이나 예비 요청과는 살짝 다른 인증 형태로 통신하게 됩니다.

1. 클라이언트에서 인증 정보를 보내도록 설정하기

기본적으로 브라우저에서 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 담지 않습니다.

이때 요청과 관련된 정보들를 담을 수 있게 해주는 옵션이 바로 credentials 옵션입니다. 3가지 값을 사용할 수 있으며, 아래와 같습니다.

옵션 값설명
same-origin(기본값)같은 출처 간 요청에만 인증 정보를 담을 수 있습니다.
include모든 요청에 인증 정보를 담을 수 있다.
omit모든 요청에 인증 정보를 담지 않는다.

만일 이러한 별도의 설정을 해주지 않으면 쿠키 등의 인증 정보는 자동으로 서버에게 전송되지 않습니다.

서버에 인증된 요청을 보내는 방법으로는 fetch 메서드를 사용하거나 axios, jQuery 라이브리리 등 다양하다. 어떤 메서드를 사용하느냐에 따라 약간 credentials 옵션을 지정하는 문법이 다르니 주의합시다.

2. 서버에서 인증된 요청에 대한 헤더 설정하기

서버도 마찬가지로 이러한 인증된 요청에 대한 일반적인 CORS 요청과는 다르게 대응해줘야 합니다.

  1. 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 합니다.
  2. 응답 헤더의 Access-Control-Allow-Origin 의 값에 와일드카드 문자("*")는 사용할 수 없습니다.
  3. 응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자("*")는 사용할 수 없습니다.
  4. 응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자("*")는 사용할 수 없습니다.

즉, Access-Control-Allow-Origin은 와일드 카드 문자가 아닌 분명한 URL이 설정되어 있어야 하고, Access-Control-Allow-Credentialstrue로 설정되어 있어야 하는 것을 의미합니다.
이렇게 설정되어 있지 않다면, CORS ERROR 메시지를 띄울 것입니다.

참고한 자료

profile
개발정리블로그

0개의 댓글

Powered by GraphCDN, the GraphQL CDN