구리

[네트워크] CORS 본문

네트워크

[네트워크] CORS

guriguriguri 2023. 6. 21. 00:15

CORS란

Cross-Origin Resource Sharing의 약자로 직역하면 교차 출처 자원 공유를 의미합니다.

 

출처(Origin)란

url 구조

출처란 서버의 위치를 찾기 위한 정보로 URL을 봤을 때, scheme(protocol) + host(domain) + port로 정의합니다.

일반적으로 http, https 프로토콜 사용시 기본 포트 번호(80, 443)가 정의되어 있어 생략이 가능하지만 출처에 포트 번호가 명시되어 있다면, 포트 번호까지 모두 일치해야 같은 출처로 인정됩니다. 하지만 이 케이스에 대한 명확한 정의가 표준으로 정해진 것은 아니기에, 경우에 따라서 같은 출처 혹은 다른 출처로 판단될 수 있습니다.

 

SOP (Same Origin Policy)

웹 생태계에서는 다른 출처로의 리소스 요청을 제한하는 것과 관련된 2가지 정책이 존재하는데 sop, cors가 이에 해당됩니다.

SOP는 같은 출처에서만 리소스를 공유할 수 있다는 규칙을 가진 정책입니다.

하지만 웹에서는 다른 출처에 있는 리소스를 요청하는 일은 많기에 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 출처가 달라도 허용하기로 했는데, 그 중 하나가 CORS 정책을 지킨 리소스 요청입니다.

따라서 다른 출처로의 리소스 요청시 SOP 정책 위반, SOP의 예외 조항인 CORS 정책까지 위반하면 다른 출처의 리소스를 아예 사용할 수 없게 됩니다.

이런 정책을 만든 이유는 무엇일까요? 

클라이언트 어플리케이션은 사용자 공격에 취약하고 서로 다른 출처의 애플리케이션이 통신하는 것에 대해 재재가 없다면 CSRF, XSS 같은 방법으로 사용자의 정보가 탈취될 가능성이 높습니다. 따라서 위와 같은 정책이 필요하게 됩니다.

같은 출처, 다른 출처를 구분하는 기준

두 URL의 구성 중 Scheme, Host, Port 3가지 정보가 동일하면 두 개의 출처가 같다고 판단합니다.

예시로 http://store.company.com/dir/page.html과 출처를 비교한 예시입니다.

URL 결과 이유
http://store.company.com/dir2/other.html 성공 경로만 다름
http://store.company.com/dir/inner/another.html 성공 경로만 다름
https://store.company.com/dir2/secure.html 실패 프로토콜 다름
http://store.company.com:81/dir2/other.html 실패 포트 다른 (http://는 80이 기본값)
http://news.store.company.com/dir2/other.html 실패 호스트 다름

여기서 중요한 사실은 출처를 비교하는 로직이 서버가 아닌 브라우저에 구현된 스펙이라는 것입니다.

만약 우리가 CORS 정책을 위반하는 리소스 요청을 해도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있지 않다면 서버는 정상 응답을 하고, 이후 브라우저가 이 응답을 분석해 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 버리게 됩니다.

즉, CORS는 브라우저 구현 스펙에 포함되는 정책으로 서버 간 통신에서는 이 정책이 적용되지 않기에 종종 Postman으로 api를 요청하면 잘 되다가 브라우저에서 api를 호출하면 CORS 에러가 발생하는 것이 이와 같은 이유 때문입니다.

 

CORS 기본 흐름

  1. 클라이언트는 HTTP 요청 헤더의 Origin 필드에 요청을 보내는 출처를 담아 서버에 요청을 전송
  2. 서버는 응답 헤더의 Access-Control-Allow-Origin필드에 "이 리소스를 접근하는 것이 허용된 출처"를 담아 응답
  3. 브라우저는 자신이 보냈던 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해 이 응답이 유효한 응답인지 판단

위는 CORS의 기본 흐름으로, CORS가 동작하는 방식은 3가지의 시나리오에 따라 변경될 수 있습니다.

(1) Preflight

  • 일반적으로 가장 많이 마주치는 시나리오로, 브라우저는 서버에게 한번에 요청을 보내지 않고 예비 요청/본 요청으로 나눠 서버로 전송
  • 이때 예비 요청을 Preflight이라고 하며, 예비 요청에는 HTTP 메소드 중 OPTIONS가 사용됨
  • 예비 요청에 대한 응답으로 서버는 Access-Control-Allow-Origin 필드를 통해 해당 리소스에 접근할 수 있는 출처를 알려줌
  • 이후 브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책 비교 후, 이 요청을 보내는 것이 안전하다 판단되는 같은 엔드포인트로 다시 본 요청을 보냄
  • 본 요청을 보내기 전, 브라우저가 스스로 이 요청을 보내는 것이 안전한지 확인하는 역할
  • 만약 예비 요청에 대한 응답이 200 OK여도 CORS 정책 위반시 CORS 에러 발생 → CORS 정책 위반으로 인한 에러는 예비 요청 성공 여부와는 상관 X
  • 예비 요청에는 본 요청에 대한 다른 정보도 포함됨
    • Access-Control-Request-Header - 본 요청에 사용될 HTTP 헤더 정보
    • Access-Control-Request-Method - 본 요청에 사용될 HTTP 메서드 정보

예비 요청, 본 요청의 흐름도

(2) Simple Requeset

  • 해당 시나리오에 대한 정식 명칭은 없으며 MDN 문서 기반으로 칭했음
  • 예비 요청 없이 서버에 본 요청을 보내면 서버 응답시 Access-Control-Allow-Origin 같은 값을 같이 보냄 → 본 요청에 대한 응답을 받은 브라우저가 CORS 정책 위반 여부를 판단
  • preflight와의 차이점은 예비 요청 유무로 나뉨
  • 특정 조건을 충족해야만 단순 요청 사용 가능 (조건이 까다롭기에 자주 사용되지 않음)
    • 요청 메소드는 GET, HEAD, POST 중 하나여야 함
    • Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안됨
    • 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
    •  

simple request 흐름도

(3) Credentialed Request

  • 인증된 요청을 사용하는 방법으로, 다른 출처 간 통신에서 보안을 더 강화하고 싶을 경우 사용됨
  • 기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않음 → 이때 요청에 인증과 관련된 쿠키 정보를 요청 헤더에 담을 수 있게 해주는 옵션이 credentials 옵션
  • 그래서 same-origin이나 include 같은 옵션을 사용해 리소스 요청시 인증 정보가 포함된다면, 브라우저는 단순히 Access-Control-Allow-Origin만 확인하는 것이 아닌 더 깐깐한 검사 조건을 추가하게 됨
credentials 옵션 값 설명
same-origin (기본값) 같은 출처 간 요청에만 인증 정보를 담을 수 있다
include 모든 요청에 인증 정보를 담을 수 있다
omit 모든 요청에 인증 정보를 담지 않는다

 

fetch('https://jy-beak.tistory.com/', {
  credentials: 'include', // Credentials 옵션 변경!
});

Access to fetch at ’https://jy-beak.tistory.com/’ from origin ’http://localhost:8000’ has been blocked by CORS policy:
The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ’*’ when the request’s credentials mode is ‘include’.

Credentialed Request 특징

위처럼 인증 정보가 담긴 상태에서 다른 출처의 리소스 요청시, 브라우저는 CORS 정책 위반 여부 검사시 룰에 다음 두가지를 추가합니다.

  • Access-Control-Allow-Origin에는 *을 사용할 수 없고 명시적인 URL을 넣어줘야 함
  • 응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 있어야 함

 

요청을 img 태그에 넣으면 어떨까?

SOP 정책에는 다른 출처의 리소스에 접근할 수 있는 몇 가지 예외조항이 존재하는데 CORS 정책을 지킨 요청을 제외하고도 스크립트, 렌더될 이미지, 스타일 시트가 존재합니다.

다른 예외 조항이 적용된 요청을 보내면 CORS를 우회할 수 있지 않을까? 라는 생각이 들 수도 있습니다.

<img src="https://jy-beak.tistory.com/rss">
<script src="https://jy-beak.tistory.com/rss"></script>

위와 같은 식으로 접근하면 CORS를 위반하지 않고 요청 자체는 성공합니다. 이때 위 요청의 헤더를 보면 Sec-Fetch-Mode: no-cors라는 값을 볼 수 있습니다.

Sec-Fetch-Mode 헤더는 요청 모드를 설정하는 필드로, 이 필드의 값이 no-cors라면 다른 출처라고 해도 CORS 정책 위반 여부를 검사하지 않습니다. 하지만 브라우저가 이 헤더에 값이 포함된 요청의 응답을 자바스크립트에게 알려주지 않기에 코드에서 절대로 이 응답에 담긴 내용에 접근할 수 없습니다.

CORS를 해결하는 방법

Server Side

  • 응답 헤더에 올바른 Access-Control-Allow-Origin 값을 세팅 (와일드카드인 *보다는 정확한 출처를 명시하는 것이 좋음)

Client Side

  • Webpack Dev Server로 리버스 프록싱하여 우회가 가능하지만 로컬 환경에서만 해결 가능함

 

 


참고

교차 출처 리소스 공유 MDN 문서

 

교차 출처 리소스 공유 (CORS) - HTTP | MDN

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라

developer.mozilla.org

evan moon님 블로그 글