[Web] CORS

JeongYong Park·2023년 7월 2일
1

이번에 프로젝트를 진행하며 CORS 정책을 만나게 되었습니다. 개발 중 흔히 겪는 문제라고 알려져 있는 CORS 에 대해 포스팅해보고자 합니다.

에러 메시지를 보면 No 'Access-Control-Allow-Origin' header 라고 나와있는데 이 메시지를 기억해두면 좋을 것 같습니다.

CORS (Cross-Origin Resource Sharing)

CORS는 말 그대로 교차 출처 리소스 공유라고 해석할 수 있습니다. 여기서 의미하는 교차 출처란 다른 출처를 의미합니다.
여기서 출처(Origin)은 무엇을 의미하길래 다른 출처라고 하는 것인지 알아보겠습니다.

Origin (출처)

웹 콘텐츠의 위치를 나타내는 URL은 아래와 같은 구성요소를 가지고 있습니다.

이때 웹 콘텐츠의 출처(Origin)는 접근할 때 사용하는 URL의 스킴, 호스트, (그림에서 나타나 있지는 않는) 포트로 정의됩니다. 두 객체의 스킴, 호스트, 포트가 모두 일치할 경우 같은 출처를 가졌다고 말할 수 있습니다.

SOP(Same-Origin Policy)

다른 출처에 대한 정책이 존재한다면 같은 출처에 대한 정책도 존재합니다. 바로 SOP 입니다.

SOP는 어떤 출처에서 불러온 리소스가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 규칙입니다.

하지만 웹 상에서 다른 출처의 리소스를 사용하는 것은 흔한 일이기 때문에 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 다른 출처라도 허용하기로 결정했습니다. 이 중 하나가 바로 CORS 정책을 지킨 리소스 요청입니다.

같은 출처와 다른 출처

그러면 어떤 것이 같은 출처이고, 어떤 것이 다른 출처 일까요?

예를 들어, https://comic.naver.com/index 이라는 URL이 있다고 가정해보겠습니다.

이 URL과 같은 출처는 다음과 같습니다.

  • https://comic.naver.com
  • https://comic.naver.com/index/1
  • https://comic.naver.com/index?hello=1

그렇다면 다른 출처는 어떤 것이 있을까요?

  • http://comic.naver.com/
    • 스킴이 달라 다른 출처입니다.
  • https://naver.com
    • 호스트가 달라 다른 출처입니다.
  • https://comic.naver.com:8080
    • 포트번호가 달라 다른 출처입니다.

CORS의 동작

이제 어떤 방법을 통해 다른 출처의 리소스를 안전하게 사용할 수 있는지 알아보겠습니다.

브라우저는 웹 서버에 요청을 보낼 때 요청 헤더의 Origin 이라는 필드에 출처를 함께 담아 보내게 됩니다.

GET / HTTP/1.1
Host: foo.example
...
Origin : http://foo.example
...

이후 이 요청에 대한 응답으로 서버는 Access-Control-Allow-Origin 값에 이 리소스에 접근하는 것을 허용된 출처에 대해 알려주게 됩니다.

HTTP/1.1 200 OK
Date: Sun, 01 Jul 2023 15:26 KST
...
Access-Control-Allow-Origin: http://foo.example

이 응답을 받은 브라우저는 자신이 보냈던 Origin 값과 서버각 응답한 Access-Control-Allow-Origin 을 비교한 후 이 응답이 유효한지 확인하게 됩니다.

기본적인 흐름은 이렇지만 CORS 접근 제어 시나리오에는 세 가지 시나리오가 있습니다.

  • 프리플라이트 요청 (Preflight Request)
  • 단순 요청 (Simple Request)
  • 인증정보 포함 요청 (Credentialed Request)

Preflight Request

Preflight request일때 브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어 보내게 됩니다.

예비 요청은 OPTIONS 헤더를 사용해 다른 도메인의 리소스로 HTTP 요청을 보내 실제 요청이 안전한지 확인합니다.

위 요청에서 preflight request는 /doc 호스트에 요청을 보내고 서버는 Access-Control-Allow-Origin : https://foo.example을 응답해주고 있습니다. foo.example 도메인을 가진 서버는 해당 리소스에 접근 가능한 출처는 오직 https://foo.example 이라는 것을 알려주는 것입니다.

클라이언트가 서버에게 보내는 요청을 살펴보면 아래와 같은 헤더가 존재합니다.

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
  • Access-Control-Request-Method : POST 는 preflight request의 일부로, 실제 요청을 전달할 때 POST로 전송된다는 것을 알려줍니다.
  • Access-Control-Request-Headers: X-PINGOTHER, Content-Type는 실제 요청을 전달할 떄 X-PINGOTHERContent-Type 사용자 정의 헤더와 함께 전송된다는 것을 서버에 알려줍니다.

이를 받은 서버는 아래와 같은 헤더들을 응답에 포함시켜 전송합니다.

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

여기서 중요한 것은 Access-Control-Allow-Origin 헤더 입니다. 서버는 이 리소스에 접근 가능한 출처는 http://foo.example 이라고 알려주는 것입니다.

이렇게 preflight request가 완료되면 본 요청을 전송하게 됩니다.

Simple Request

단순 요청은 프리플라이트 요청과 다르게 예비요청 없이 본 요청을 바로 보내고, 서버가 이에 대한 응답으로 Access-Control-Allow-Origin과 같은 값을 보내주면 CORS 정책 위반 여부를 확인하는 방법입니다.

프리플라이트 요청보다 단순해보이지만 아래와 같은 조건을 모두 만족해야 합니다.

  • 다음 중 하나의 메서드
    • GET, HEAD, POST
  • 유저 에이전트가 자동으로 설정한 헤더외에, 수동으로 설정할 수 있는 헤더는 오직 Fetch 명세에서 “CORS-safelisted request-header”로 정의한 헤더 뿐입니다.
    • Accept, Accept-Language, Content-Language, Content-Type
    • 이 중 Content-Type 헤더는 application/x-www-form-urlencoded, multipart/form-data, text/plain 만 허용합니다.
    위 조건을 모두 만족시키기는 어렵습니다. 대부분의 HTTP API는 application/json 타입을 가지도록 설계되기 때문에 이를 만족시키는 상황은 나오기 어려울 것 같습니다.

Credentialed Request

인증정보를 포함한 요청은 HTTP cookiesHTTP Authorization 정보를 인식합니다. 그렇기 때문에 다른 출처 간 통신에서 보안을 강화하고 싶을 때 사용합니다.

기본적으로 XMLHttpRequestFetch 호출에서 브라우저는 인증정보를 보내지 않습니다. 그렇기 때문에 XMLHttpRequest 객체나 Request 생성자가 호출될 때 특정 플래그를 설정해야 합니다.

fetch('http://foo.example', {
  credentials: 'include'
});

credentials 정보를 include로 설정하게 되면 브라우저가 응답을 줄 때 Access-Control-Allow-Credentials: true 헤더가 없는 응답을 거부합니다.

CORS 해결하기

백엔드 개발자로서 CORS 에 대처하기 위해서는 Access-Control-Allow-Origin 헤더에 알맞은 값을 세팅해 주는 것입니다. 이때 와일드 카드 * 을 사용하면 당장은 편할 수 있겠지만 모든 출처에서 오는 요청을 받겠다는 의미이기 때문에 좋지 않습니다. 따라서 출처를 명시해주는 것이 좋습니다.

결론

  • CORS는 Origin, Access-Control-Allow-Origin HTTP 헤더등을 사용해서 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 방식이다.
  • CORS의 동작 시나리오에는 Preflight Request, Simple Request, Credentialed Request 가 있다.
  • 서버 입장에서 CORS 를 해결하기 위해서는 Access-Control-Allow-Origin 헤더에 알맞은 값을 세팅해주어야 한다.

참고 자료

https://developer.mozilla.org/ko/docs/Glossary/Origin

https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

profile
다음 단계를 고민하려고 노력하는 사람입니다

0개의 댓글