[네트워크] CORS (SOP, error해결)

임승민·2023년 4월 29일
1

네트워크

목록 보기
8/9
post-thumbnail

CORS란

CORS는 Cross Origin Reasource Sharing의 약자로 직역하면 교차 출처 리소스 공유라는 뜻이다.

CORS는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.

쉽게 말해 다른 출처의 리소스 공유에 대한 허용/비허용 정책이다.

여기서 교차 출처다른 출처를 말한다. 그럼 출처(Origin)는 무엇일까?

URL은 하나의 문자열이 아닌 여러 구성요소로 이루어져있다. 출처(Origin)는 Protolcol, Host, Port를 합친 URL을 의미한다.

SOP(Same-Origin Policy)

SOP는 동일 출처 정책이란 뜻으로 ‘동일한 출처에서만 리소스를 공유할 수 있다’ 라는 규칙을 가지고있다. 따라서 브라우저는 다른 출처의 자원에 접근 하는 것을 차단한다.

만약 SOP 정책이 없다면 해커가 CSRF나 XSS 등의 방법을 이용해 개인 정보를 탈취할 수 있다.
하지만 그렇다고 다른 출처로 리소스를 요청할 수 없는건 아니다. CORS 정책만 지킨다면 가능하다.

  • 출처 비교 로직은 서버가 아닌 브라우저에 구현된 스펙이다. 따라서 브라우저 없이 서버 간 통신하면 CORS 정책이 적용되지 않는다.

CSRF (cross site request forgery attack)
사용자의 의지와 무관하게 공격자가 의도한 행동을 특정 웹페이지에 요청하게 하는 공격 방법이다.
XSS (Cross-site Scripting)
공격자가 상대방의 웹 사이트에 악의적 스크립트를 삽입하는 공격 방법이다.

CORS 기본 동작

  1. 클라이언트에서 HTTP요청 헤더에 Origin필드에 출처를 담아 전달한다.
  2. 서버는 응답헤더에 Access-Control-Allow-Origin 필드를 추가하고 값으로 '이 리소스를 접근하는 것이 허용된 출처 url'을 전달한다.
  3. 응답 받은 브라우저는 요청 Origin과 응답 받은 Access-Control-Allow-Origin을 비교해 차단 여부를 결정한다.
    a. 유효하면 다른 출처의 리소스를 가져온다.
    b. 유효하지 않다면 응답을 사용하지 않고 버린다. (CORS 에러)

결론은 서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답하면 된다.

CORS 작동 방식 3가지

CORS 동작 방식은 한가지가 아니라 3가지 시나리오 따라 변경된다.

예비 요청 (Preflight Request)

브라우저는 본 요청 전 예비 요청을 통해 이 요청을 보내는 것이 안전한지 확인한다.

  • 예비 요청의 메소드는 OPTIONS 요청이 사용된다.
  1. JS fetch 메서드를 통해 리소스를 받아오려고 한다.
  2. 브라우저는 서버로 HTTP OPTIONS 메소드로 예비 요청을 먼저 보낸다.
    • Origin 헤더: 자신의 출처
    • Access-Control-Request-Method 헤더: 실제 요청에 사용할 메소드
    • Access-Control-Request-Headers 헤더: 실제 요청에 사용할 헤더들
  3. 서버는 예비 요청 응답으로 어떤 것을 허용/ 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보낸다.
    • Access-Control-Allow-Origin 헤더: 허용되는 Origin들의 목록
    • Access-Control-Allow-Methods 헤더: 허용되는 메소드들의 목록
    • Access-Control-Allow-Headers 헤더: 허용되는 헤더들의 목록
    • Access-Control-Max-Age 헤더: 해당 예비 요청이 브라우저에 캐시 가능한 시간(초 단위)
  4. 브라우저는 요청과 응답 정책을 비교해 요청이 안전한지 확인하고 본 요청을 보낸다.
  5. 서버가 본 요청에 응답하면 최종적으로 이 응답 데이터를 JS로 넘겨준다.

문제점

예비 요청으로 인해 실제 요청 시간 증가와 서버 요청이 배로 발생해 비용, 성능 문제가 생긴다.

하지만 이는 서버로 부터 Access-Control-Max-Age 응답 헤더를 받아 해당 시간동안 브라우저 캐시에 결과를 저장해 해결할 수 있다.

단순 요청 (Simple Request)

단순 요청은 예비 요청 없이 본 요청을 보낸 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS정책 위반 여부를 검사하는 방식이다.

하지만 단순 요청은 3가지 경우를 만족해야 한다.

  1. 요청 메소드가 GET, HEAD, POST 중 하나
  2. AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth 헤더일 경우
  3. Content-Type 헤더가 application/x-www-form-urlencodedmultipart/form-datatext/plain 중 하나

위 조건은 까다롭고 대부분 HTTP API 요청은 text/xml 이나 application/json 으로 통신해서 Content-Type에 위반된다. 따라서 대부분 예비 요청이 일어난다.

인증된 요청 (Credentialed Request)

인증된 요청은 클라이언트가 서버로 자격 인증 정보(쿠키, 토큰 등)를 실어 요청할때 사용되는 요청이다.

또한 예비 요청 처럼 preflight가 먼저 일어난다.

기본적으로 브라우저가 제공하는 요청 API 들은 인증 관련된 데이터를 요청 데이터에 담지 않도록 되어있지만 credentials 옵션으로 요청에 인증과 관련된 정보를 담을 수 있다.

이 옵션은 3가지의 값을 사용할 수 있다. 별도 설정을 하지 않으면 인증 정보는 서버에 자동 전송되지 않는다.

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

인증된 요청을 보내는 방법은 fetch 메서드나 axios, jQuery 라이브리리 등이 있다.

서버도 인증된 요청에 대해 일반적인 CORS 요청과는 다르게 대응해야 한다.

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

CORS 해결 방법

BEST👍🏻) Access-Control-Allow-Origin 응답 헤더 세팅

서버측에서 Access-Control-Allow-Origin 헤더에 유효한 값을 포함해 브라우저에 응답하면 된다.

또한 Access-Control-Allow-Origin 값에 와일드카드 문자("*")를 사용하면 모든 출처의 요청을 받아 보안적 이슈가 발생하기에 분명한 Origin을 설정해야 한다.

  • 서버측 응답에서 접근 권한을 주는 헤더를 추가하여 해결한다.
// CORS관련 HTTP 헤더 값
Access-Control-Allow-Origin
Access-Control-Request-Methods
Access-Control-Allow-Headers
Access-Control-Max-Age
Access-Control-Allow-Credentials
Access-Control-Expose-Headers

프록시 사이트 이용하기

요청해야 하는 URL 앞에 프록시 서버 URL을 붙여서 요청하게 된다. 프록시 서버를 사용하면 중간에 요청을 가로채서 HTTP 응답헤더에 Access-Control-Allow-Origin : * 를 설정해준다.

// heroku 프록시 서버 URL
https://cors-anywhere/herokuapp.com
// 요청
axois({
  method: "GET",
  url: `https://cors-anywhere/herokuapp.com/{주소}`,
  header:{
	'APIKey': ${API_key}
  }
})

현재 무료 프록시 서비스들은 모두 악용 사례로 api 요청 횟수 제한을 두어 실전이 아닌 테스트용으로 사용해야 한다. 실전에서는 프록시 서버를 구축하여 사용해야 한다.

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

webpack-dev-server가 제공하는 프록시 기능을 사용한다. 이는 로컬에서만 가능하다.

아래의 설정을 하면 /api로 시작하는 URL로 보내는 요청을 브라우저는 localhost:8000/api로 요청한줄 알지만 실은 웹팩이 target값인 URL로 요청을 프록싱한다.

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.evan.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
      },
    }
  }
}

http-proxy-middleware 사용

로컬 환경에서 사용하여 클라이언트단에서 쉽게 해결할 수 있다.

http-proxy-middleware를 설치하고 src 폴더안에setupProxy.js 파일을 만들고 아래 코드를 작성하면 된다. 그럼 로컬 환경에서 http://localhost:3000/api로 시작하는 요청을 라이브러리가 http://localhost:5000/api로 프록싱 해준다.

const { createProxyMiddleware } = require("http-proxy-middleware")

module.exports = function (app) {
	app.use(
		"/api",
		createProxyMiddleware({
			target: "http://localhost:5000",
			changeOrigin: true,
		})
	)
}

package.json에 proxy값 설정

CRA로 생성한 프로젝트에서는 package.json에 proxy 값을 설정해 proxy기능을 활성화할 수 있다.

{
  //...
  "proxy": "http://localhost:4000"
}

JSONP

브라우저에서 css나 js 같은 리소스 파일들은 SOP 영향을 받지않아 외부 서버에서 읽어온 js 파일을 json으로 바꿔주는 일종의 편법이다. 단점은 GET 방식의 API만 요청이 가능하다.


References

https://inpa.tistory.com/entry/WEB-📚-CORS-💯-정리-해결-방법-👏

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

https://ingg.dev/cors

https://xiubindev.tistory.com/115

https://simsimjae.medium.com/cors와-jsonp에-대해서-aa3ec0456e97

http://yoonbumtae.com/?p=2452

0개의 댓글