프로그래밍 기초

CORS란 (Origin, SOP, CORS, CSP)

hs-archive 2023. 7. 19. 16:32

들어가기 전에...

교차 출저 리소스 공유(Cross Origin Resource Sharing, CORS)를 알기 위해서는 동일 출처 정책(Same Origin Policy, SOP)과 출처(Origin)가 무엇인지에 대한 지식이 있어야 합니다.

 

따라서 이번 포스팅에서는 순차적으로 Origin이 무엇인지 배우고 SOP가 무엇인지 배운 뒤 CORS에 대해 배워보겠습니다.

 

Origin

출처(Origin)는 CORS와 SOP의 O에 해당하는 Origin을 뜻합니다. 이는 URL의 프로토콜(스킴이라고도 함), 도메인, 포트로 정의 됩니다. 두 객체의 프로토콜, 호스트, 포트가 모두 일치하는 경우 같은 출처라고 말합니다.

 

http://example.com:80/app1/index.html

 

위와 같은 URL이 있을 때 프토토콜, 도메인, 포트 번호에 해당하는 부분은 각각 아래와 같습니다. 참고로 app1/index.html은 경로라고 합니다. 

 

  - 프로토콜에 해당하는 부분은 http://

  - 도메인에 해당하는 부분은 example.com

  - 포트 번호에 해당하는 부분은 :80

 

위에서 언급했던 것처럼 프로토콜 + 도메인 + 포트 번호가 같다면 같은 Origin으로 취급합니다. 따라서 아래 두 URL은 같은 출처입니다.

http://example.com:80/app1/index.html

http://example.com:80/abcde

 

SOP(Same Origin Policy)

동일 출처 정책(Same Origin Policy, SOP)는 말 그대로 하나의 정책입니다. SOP는 웹 보안 정책 중 하나로, 웹 브라우저에서 실행되는 스크립트나 리소스가 다른 출처로부터 로드된 리소스와 상호 작용하지 못하도록 하는 보안 정책입니다. SOP는 다른 출처와 상호작용하는 것을 막음으로써 악의적인 사이트로부터 사용자의 정보를 보호하고 웹 보안을 강화하는 역할을 합니다.

 

이러한 SOP는 브라우저의 기본적인 정책입니다. 따라서 다른 출처에 대한 요청으로 받은 리소스는 사용할 수 없는 것이 기본입니다. 그런데 생각해 보면 대부분의 웹 개발은 프론트 서버와 백엔드 서버가 나누어져 있으므로 당연히 그 둘의 출처가 다를 수밖에 없습니다. 이러한 상황에서 SOP를 준수한다면 프론트는 페이지를 구성하는 데 필요한 데이터를 백엔드 서버로부터 얻을 수가 없습니다. 따라서 SOP에는 몇가지 예외가 존재합니다. 대표적인 예외는 다음과 같습니다.

 

  - <link>: link 태그의 href에서 다른 출처의 .css 리소스를 가져와서 사용하는 것을 허용합니다.

  - <img>: img 태그의 src에서 다른 출처의 .png, .jpg 등의 리소스를 가져와서 사용하는 것을 허용합니다. 

  - <script>: script 태그이 src에서 다른 출처의 .js 리소스를 가져와서 사용하는 것을 허용합니다.

  - 교차 출처 리소스 공유(CORS)를 사용한 경우

 

위에 적힌 태그를 사용하여 API를 호출하면 정상적으로 값을 받기는 하지만 브라우저가 해당 값을 자바스크립트에게 알려주지 않습니다. 즉, 우리는 코드 레벨에서 절대 이 응답에 담긴 내용에 접근할 수 없는 것입니다. 따라서 다른 출처로의 호출을 진행한 뒤 해당 요청에 대한 반환값을 프론트에서 받아서 이리저리 사용하려면 CORS를 통해서 요청을 하는 방법뿐입니다.

 

CORS(Cross-Origin Resource Sharing) - 참고

CORS - 대략의 그림

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처(Origin)에서 실행 중인 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 CORS HTTP 요청을 실행합니다.

 

CORS는 세 가지 시나리오가 있습니다. 이 세 가지 시나리오에 대해 알아본 뒤 CORS 에러를 해결하는 방법에 대해 배워봅시다.

 

Preflight Request 방식

Preflight Request 방식

프리플라이트(Preflight) 방식은 일반적으로 우리가 웹 애플리케이션을 개발할 때 가장 많이 마주치는 시나리오입니다.

 

프리플라이트는 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 뜻합니다. 이 예비 요청에는 HTTP 메서드 중 OPTIONS 메서드가 사용됩니다. 예비 요청의 목적은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내도 되는지 확인하는 것입니다.

 

프리플라이트 방식의 동작은 아래와 같습니다.

 

  - 브라우저는 다른 Origin으로 요청을 보낼 때 요청의 Origin이라는 헤더에 자신의 Origin 적어서 서버에게 보냅니다.

  - 서버는 응답의 Access-Control-Allow-Origin이라는 헤더에 사전에 설정해 놓은 허용된 출처 목록을 적어서 반환합니다.

  - 해당 응답을 받은 브라우저는 Access-Control-Allow-Origin의 허용 목록에 본인의 Origin이 있다면 본 요청을 진행하고 없다면 더 이상 요청을 진행하지 않습니다.

 

참고로, 허용 목록에 Origin이 없을 때 HTTP의 요청이 중간에 소실되거나 서버에서 응답을 거부하는 것이 아니라 요청과 응답은 정상적으로 이루어졌으나 브라우저가 origin을 확인하여 해당 응답을 더 이상 진행하지 않는 것이므로 HTTP response status code는 200입니다.

 

그리고, 이러한 예비 요청은 분명한 오버헤드입니다. 따라서 서버의 Access-Control-Allow-Origin을 캐싱해 두었다가 예비 요청을 중복으로 보내지 않고 캐싱 된 값을 확인하여 요청을 진행하는 것이 더 효율적입니다. 서버는 AccessControl-Max-Age라는 헤더를 사용하여 브라우저에게 캐싱을 허용할 수 있습니다. 서버가 보낸 응답에 있는 AccessControl-Max-Age 헤더의 값이 7200인 경우 브라우저는 2시간 동안 해당 값을 캐싱해 두며 그 시간 이후에는 더이상 캐싱 된 값이 유효하지 않기 때문에 예비 요청을 다시 보냅니다.

 

Simple Request 방식

Simple Request 방식

단순 요청은 예비 요청을 보내지 않고 바로 서버에게 본 요청부터 보낸 뒤, 서버가 이에 대한 응답 헤더에 Access-Control-Allow-Origin과 같은 값을 보내주면 그때 브라우저가 CORS 정책 위반 여부를 검사하는 방식입니다. 즉, 프리플라이트 방식과 단순 요청 방식의 차이점은 예비 요청 존재 여부입니다.

 

아무 때나 이러한 단순 요청 방식을 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 이렇게 예비 요청을 생략할 수 있습니다.

 

  - 요청의 메서드는 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만 허용됩니다.

 

복잡한 상용 웹 애플리케이션은 보통 위에 언급된 헤더들 외에 추가적인 헤더를 사용하므로 이러한 조건을 충족시키는 경우는 드뭅니다. 예를 들어 Authrization 헤더를 사용하는 경우 단순 요청 방식을 사용할 수 없습니다. 게다가 대부분의 HTTP API는 text/xml이나 application/json 컨텐츠 타입을 갖도록 설계되기 때문에 사실상 이 조건을 충족시키기는 어렵습니다.

 

Credentialed Request 방식

세 번째 시나리오는 인증된 요청을 사용하는 방식입니다. 이 시나리오는 CORS의 기본적인 방식이라기보다 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방식입니다.

 

기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않습니다. credentials 옵션은 이러한 인증과 관련된 정보를 자동으로 요청에 담아주는 역할을 합니다.

 

이 옵션에는 총 3가지의 값을 사용할 수 있으며, 각 값이 갖는 의미는 다음과 같습니다.

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

 

만약 same-origin이나 include 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin만 확인하는 것이 아니라 위 옵션에 따른 조건이 추가되는 것입니다.

 

Access-Control-Allow-Origin이 *로 모든 출처로의 요청을 허용하는 서버가 있다고 생각해 봅시다. 해당 서버는 모든 출처로의 요청을 허용하므로 해당 서버로 요청을 보내면 당연히 해당 값을 받아서 브라우저에서 확인할 수 있을 것입니다. 브라우저에서 서버로 보내는 요청의 credentials 헤더를 'include'로 변경하여 서버로 보낸다면 아래와 같은 에러가 발생합니다.

🚨 Access to fetch at ’server url’ 
from origin ’from url’ 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’.

 

에러를 보면 브라우저의 헤더에 담긴 credentials 모드가 include인 경우 서버의 Access-Control-Allow-Origin의 값이 *면 안 된다고 말하고 있습니다.

 

이처럼 요청에 인증 헤더 정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면 브라우저는 CORS 정책 위반 여부를 검사하는 룰에 다음 두 가지를 추가하게 됩니다.

 

  - Access-Control-Allow-Origin에는 *를 사용할 수 없습니다.

  - 응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야 합니다.

 

이렇게 인증까지 얽혀있는 경우 다른 시나리오에 비해 다소 복잡하지만, 이렇게 CORS 정책에 대한 다양한 시나리오를 알아두면 실제 상황에서 CORS 정책 위반으로 인한 문제가 발생했을 경우 삽질 시간을 크게 단축할 수 있으니 숙지해 놓는 것이 좋습니다.

 

CORS 정책 위반 해결하기

지금까지 CORS가 무엇인지, CORS 정책이 적용되는 시나리오는 무엇인지에 대해 배웠습니다. 이번에는 CORS 정책 위반으로 에러가 발생했을 때 그 문제를 해결하는 방법에 대해 배워보겠습니다.

 

Access-Control-Allow-Origin 설정하기 

위 CORS 시나리오를 보면 서버의 Access-Control-Allow-Origin에 의해 브라우저가 해당 반환 값을 사용할지 안 할지가 정해집니다. 따라서 서버에서 Access-Control-Allow-Origin 헤더에 알맞은 값을 세팅해주면 됩니다.

 

이때 모든 출처를 허용하는 `*`를 사용하는 것은 편하긴 하지만 정체도 모르는 이상한 출처에서 오는 요청까지 허용한다는 말이므로 다음과 같이 명시적으로 Access-Control-Allow-Origin를 설정하는 것이 좋습니다.

Access-Control-Allow-Origin: https://hs-archive.tistory.com/

  

이 헤더는 Nginx나 Apache와 같은 서버 엔진의 설정에서 추가할 수도 있지만, 아무래도 복잡한 세팅을 하기에는 불편하기 때문에 소스 코드 내에서 응답 미들웨어 등을 사용하여 세팅하는 것이 좋습니다. Spring, Express, Django와 같은 프레임워크의 경우 모두 CORS 관련 설정을 위한 라이브러리를 제공해 줍니다.

 

Webpack Dev Server로 리버스 프록싱하기 

사실 CORS를 가장 많이 마주치는 환경은 바로 로컬에서 프론트엔드 애플리케이션을 개발하는 경우라고 해도 과언이 아닙니다. 백엔드에는 이미 Access-Control-Allow-Origin 헤더가 세팅되어 있겠지만, 이 중요한 헤더에다가 'http://localhost:3000' 같은 범용적인 출처를 넣는 경우는 드물기 때문입니다.

 

프론트엔드 개발자는 대부분 웹팩과 webpack-dev-server를 사용하여 자신의 머신에 개발 환경을 구축하게 되는데, 아래와 같이 이 라이브러리가 제공하는 프록시 기능을 사용하면 아주 편하게 CORS 정책을 우회할 수 있습니다.

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
      },
    }
  }
}

 

이렇게 설정을 해놓으면 로컬 환경에서 /api로 시작하는 URL로 보내는 요청에 대해 브라우저는 localhost:3000/api로 요청을 보낸 것으로 알고 있지만, 사실 뒤에서 웹팩이 http://localhost:8080으로 요청을 프록싱해주기 때문에 마치 CORS 정책을 지킨 것처럼 브라우저를 속이면서도 우리는 원하는 서버와 자유롭게 통신할 수 있게 됩니다. 즉, 프록싱을 통해 CORS 정책을 우회할 수 있는 것입니다. 아래 그림을 보시면 프록시를 사용했을 때의 전체적인 요청 흐름을 파악할 수 있습니다. 

 

프록시를 사용한 CORS 에러 우회 과정 - https://junhyunny.github.io/information/react/react-proxy/

 

정리

CORS와 SOP는 출처에 대한 브라우저의 보안 정책입니다. 특히, SOP는 동일 출처에 대한 리소스만 허용하는 브라우저의 기본적인 보안 정책입니다. 하지만 현대 웹 애플리케이션에서 동일 출처로의 리소스만 허용할 수는 없으므로 예외를 두는데, CORS가 그것입니다.

 

CORS는 웹 페이지 다른 출처의 리소스를 사용할 수 있도록 하는 구조이며 대부분의 경우 preflight 방식으로 진행됩니다.

 

CORS를 위반하여 브라우저가 서버로부터 받은 응답을 버리는 일이 발생하면, 서버의 Access-Control-Allow-Origin에 해당 클라이언트의 출처를 허용해 주거나 로컬 개발 환경의 경우 클라이언트가 직접 프록싱을 해서 문제를 해결할 수 있습니다.

 

덧붙여...

CSP

CSP는 위 그림과 같이 공격자가 서버의 응답을 가로채서 자신의 악성 스크립트를 심은 뒤 클라이언트에게 전달하는 것을 위한 정책입니다. CSP는 스크립트 혹은 이미지, 스타일에게 해시값이나 일정한 난수 값을 요구하여 상기 XSS 공격을 방지합니다. 따라서, 공격자가 중간에서 응답을 가로챈 뒤 악성 스크립트를 심더라도 해당 스크립트에 해시값이나 난수 값을 함께 넣지 않는 이상 악성 스크립트는 브라우저에서 실행되지 않습니다.

 

CSP도 어떠한 스크립트, 스타일, 이미지 등의 실행을 '차단'하여 보안을 강화하고, CORS도 허용되지 않은 출처를 '차단'하여 보안을 강화하므로 둘 다 '차단'을 통한 보안 강화라는 공통점이 있지만, CORS는 동일 출처 원칙과 관련된, 다른 출처의 리소스를 브라우저에 받아들일 것인가 말 것인가 정하는 거라면, CSP는 그렇게 받아온 여러 스크립트 중 어느 것을 실제 실행시킬 것인지에 대한 정책입니다. 

 

 

 

 

 


https://evan-moon.github.io/2020/05/21/about-cors/

 

CORS는 왜 이렇게 우리를 힘들게 하는걸까?

이번 포스팅에서는 웹 개발자라면 한번쯤은 얻어맞아 봤을 법한 정책에 대한 이야기를 해보려고 한다. 사실 웹 개발을 하다보면 CORS 정책 위반으로 인해 에러가 발생하는 상황은 굉장히 흔해서

evan-moon.github.io

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F

 

🌐 악명 높은 CORS 개념 & 해결법 - 정리 끝판왕 👏

악명 높은 CORS 에러 메세지 웹 개발을 하다보면 반드시 마주치는 멍멍 같은 에러가 바로 CORS 이다. 웹 개발의 신입 신고식이라고 할 정도로, CORS는 누구나 한 번 정도는 겪게 된다고 해도 과언이

inpa.tistory.com

https://it-eldorado.tistory.com/163

 

[Web] CORS (Cross Origin Resource Sharing) 이해하기

이번 포스팅에서 다룰 내용은 바로 CORS(Cross Origin Resource Sharing)이다. 웹 개발자라면 한 번쯤은 CORS와 관련하여 콘솔에 뜨는 빨간 글씨의 에러 때문에 짜증 났던 적이 있을 것이다. 하지만 CORS 정책

it-eldorado.tistory.com

https://junhyunny.github.io/information/react/react-proxy/

 

React 개발 서버 CORS 해결하기 with Proxy

<br /><br />

junhyunny.github.io

 

'프로그래밍 기초' 카테고리의 다른 글

Socket.IO 소개 2 - Event  (0) 2023.07.28
Socket.IO 소개 1 - Server  (0) 2023.07.27
실시간 통신 - WebSocket이란  (0) 2023.07.18
트랜잭션의 격리성과 격리 수준  (0) 2023.06.27
데이터베이스 트랜잭션  (0) 2023.06.21