CORS

newhyork·2022년 4월 26일
0

CORS


Cross Origin Resource Sharing

  • 다른 출처(서버가 선택한)로부터의 리소스를 사용할 수 있도록 허용하는 정책.
  • 서버 측에서 '브라우저'에게 알려주는 것이다.
    즉, CORS 위반 여부는 ‘브라우저’가 관여해서 판단한다.
    서버가 브라우저에게, 자신이 허용한 다른 출처를 알려준다.
  • 클라이언트(좀 더 정확히는, JS의 fetch 등) < - - > 브라우저 < - - > 서버 인 구조.
  • 브라우저의 구현 spec에 포함되는 정책일 뿐이기에
    브라우저를 통한 통신이 아닐 때는 이 정책이 적용되지 않는다.
    • 따라서 아래에서 살펴보겠지만, 클라이언트와 서버 간에 origin이 다르면
      proxying을 통해 CORS와 관계없이 통신이 가능하게 하는 방법이 있다.

origin(출처)


  • scheme(protocol), host, port 의 조합.
    • ex) http://localhost:80인 URL에서는
      http가 scheme(protocol), localhost가 URL, 80이 port 이다.
    • 위 세 가지 중 하나라도 다르다면 다른 출처로 취급한다.
      모두 같아야 같은 출처이다.
      (http는 그 자체가 80 port를 의미하니, 80은 생략해도 된다.)
  • http://localhosthttp://127.0.0.1
    • 이 둘은 의미하는 바는 같지만, 브라우저는 string value로 비교하기에 다르다고 판단한다.
  • 클라이언트의 리소스, 서버의 리소스 등 모든 리소스에는 출처가 있기 마련인데,
    CORS를 살펴보기 위해서, 여기서는 클라이언트 측의 리소스를 주목한다.
    아니 그 보다 더, 클라이언트의 '출처' 라는 것에 주목한다.

SOP


Same Origin Policy

  • 어떤 특정한 출처인 곳(서버)에서,
    다른 출처(클라이언트)로부터의 리소스를 사용하지 못하게 제한하는 보안 정책이다.
    즉, 동일한 출처의 리소스만 허용하는 브라우저의 보안 정책이다.
    • 겉으로는 서버 입장에서 안전한 출처의 리소스만 받도록 하는 게 목적으로 보이지만
      좀 더 정확히 말하자면 궁극적인 목적은 결국 사용자를 위한 정책이다.
      예를 들어, 해커가 어떤 웹 사이트와 그럴싸하게 비슷한 악성 웹 사이트를 만들어
      사용자를 유도하고, 사용자의 인증 정보로 서버에 요청을 보내어
      응답으로 사용자의 민감한 정보를 탈취하고자 한다.
      하지만 서버와 해커가 만든 웹 사이트는 cross-origin이기 때문에
      브라우저에서는 서버의 응답을 클라이언트에게 전달하지 않는다.
      (CORS에 의해서도 서버가 허락하지 않은 출처이면 막히는건 마찬가지다)
  • Request Headers의 origin이 서버인 origin과 동일한지 확인하는 것이다.
    cross-origin인 경우 제한하고, same-origin만 허용한다.
  • 클라이언트로부터 요청이 오면, 일단 서버에서는 그 요청에 따라 처리를 한다.
    이후, 브라우저가 request header의 origin과 response header의 origin(서버)를 비교 확인하고,
    출처(origin)의 일치 여부에 따라 클라이언트에게 서버의 응답을 전달한다.
    • 즉, 서버에서는 이미 클라이언트의 요청을 처리는 한 상태이다.
    • 아래에서 살펴볼 CORS 시나리오의 simple request랑 다를 게 뭔가 싶을텐데,
      SOP에서와 달리 CORS 하에서는 DELETE 등 특정 method에 대해서는 simple request를 하지 않는다.
    • 위에서 살펴본 것과 같이 SOP의 한계는, 서버가 다른 출처로부터의
      요청(DELETE를 비롯한 모든)을 처리 자체는 한다는 것이다.
      (사실 이 부분에 대해 알아보면서 설마 이렇게 동작했을까 싶은 생각이 들었다.
      그래서 아직 명확하게 받아 들여지지는 않았다.
      혹시 SOP만 있던 시절에는 이러한 부분에 대해서
      서버 내에 로직을 통해서 다른 출처이면 필터링을 했을지,
      혹은 브라우저에서 Request Headers의 origin과 host를 보고 에러를 냈을지
      감히 짐작만 해보는 것으로 넘어가도록 한다.)
  • 외부 API를 이용한다거나 클라이언트와 서버를 각각 다른 port로 배포해서 개발하는 경우 등은
    다른 출처의 리소스를 이용하는 것이 되기 때문에, 어쩔 수 없이 SOP를 위반하게 된다.
    그래서 이걸 해결하기 위해 나온 것이 CORS 정책이다.
    • Flask로 웹 개발을 할 시 화면으로 jinja2 template을 쓰는 프로젝트의 경우,
      동일한 출처이기 때문에 SOP를 따르므로 CORS를 만날 일이 없다.

CORS 접근 시나리오


  • CORS 정책에 따라, 서로 다른 출처인 클라이언트와 서버가 통신하기 위해서,
    클라이언트 측에서는 요청을 보낼 때 세 종류의 request방식 중 하나로 요청을 보내게 되는데,
    어떤 방식으로 request를 하던 간에 결국 브라우저가 관여하여 CORS를 판단한다.
  • request는 origin 및 필요에 따라 Access-Control-~ 등의 헤더를 담고 있고,
    response는 Access-Control-Allow-Origin 및 필요에 따른 헤더를 담고 있다.
    • 브라우저는 request의 origin과 response의 Access-Control-Allow-Origin가 일치하는지를 확인하여 서버가 허용한 출처인지 알 수 있고, 이에 따라 클라이언트의 요청에 따른 서버의 응답을 클라이언트에게 전달해준다.
  • 세 방식 중 어떤 것을 사용할지는 request를 분석하여(method 등) 브라우저가 판단한다.

simple request

  • 요청이 다음 조건들을 모두 만족하면 simple request 방식으로 요청하게 된다.
    • GET, POST, HEAD method 이어야 한다.
    • POST일 경우 content-type에 application/~, multipart/form-data, text/plain 만 허용한다.
    • 헤더는 Accept, Accept-Language ~ 만 허용. 그 외 커스텀 헤더가 없어야 한다.
  • 혹시 서버에서 요청을 처리했다고 하더라도 브라우저의 판단 하에
    서버에서 허용하지 않은 출처인 경우, 클라이언트로 응답을 전달하지 않는 경우에 해당된다.
    기존 데이터에 side effect를 일으키지 않는 요청이기 때문에,
    (OPTIONS없이 바로) 실제 요청을 날리면서 cross-origin인지 확인해도 된다.

preflight request

  • 요청이 simple request의 조건을 모두 만족하지 않는 경우,
    preflight request 방식으로 요청하게 된다.
    • 우선 '브라우저가' OPTIONS 메서드를 통해,
      (클라이언트에서 따로 OPTIONS method를 작성하지 않았다!)
      request에 해당되는 다른 출처의 리소스로 요청이 가능한지 서버로부터 확인을 받고자한다.
      이후, 브라우저가 서버로부터 OPTIONS 요청에 대해 200을 받으면, 그제서야 클라이언트의 실제 요청을 보낸다.
      • 사실 OPTIONS의 응답 결과가 200/400 이냐는 중요하지 않다.
        Access-Control-Allow-Origin 값이 중요하다.
        이걸 토대로 브라우저가 CORS에러를 위반 여부를 판단하고, 실제 요청을 보내게 된다.
  • OPTIONS request에는 다음 것들이 필요에 따라 요구된다.
    • origin: 요청 출처(클라이언트)
    • Access-Control-Request-Method: 이후 실제 요청의 메서드
    • (Access-Control-Request-Headers: 이후 실제 요청에 추가할 헤더)
  • OPTIONS response에는 다음 것들이 필요에 따라 요구된다.
    • Access-Control-Allow-Origin: 서버측 허가 출처
    • Access-Control-Allow-Methods: 서버측 허가 메서드
    • (Access-Control-Allow-Headers: 서버측 허가 헤더)
    • (Access-Control-Max-age: preflight 응답을 캐싱하는 기간.
      한 번 200으로 통과했으면, 이 기간 동안은 더 이상의 OPTIONS는 요청하지 않아도 된다)

credential request

  • 요청에 인증 관련 헤더(쿠키)를 포함할 때 credential request 방식으로 요청하게 된다.
    로그인을 하고 나서 사용할 수 있는 기능에 대해서는 request 시 인증이 필요하기 때문이다.
  • 클라이언트측에서는 withCredentials: true 따위의 설정을 해줘야 하고,
    서버 측에서도 Access-Control-Allow-Credentials: true 따위의 설정을 해줘야 한다.
    • 이 때, Access-Control-Allow-Origin에 *(모두 허용)는 하지 않고, 직접 명시해주는 게 좋다.

CORS 에러 해결 방법


proxying

  • package.json이나 webpack의 proxy 설정을 통해 구현하거나
    Nginx를 reverse proxy로 사용할 수 있다는 점을 이용해 구현할 수 있다.
    • 주로 전자는 로컬 환경에서 개발할 때 이런 식으로 간단하게 해결할 때 쓰고,
      후자는 같은 도메인에 port를 달리해서 프론트와 백엔드를 배포 시 사용한다.
      예를 들어, React APP과 Flask APP을 배포하는 경우를 떠올려보면 된다.
      로컬에서 package.json이나 webpack을 통해 proxying을 한 것처럼,
      Nginx가 그 역할을 한다고 생각하면 된다.
  • 브라우저 입장에서 same-origin이라고 생각하도록,
    클라이언트는 proxy를 통해 서버로 요청을 보낸다.
    • 클라이언트의 요청이 proxy로 보내질 때는 브라우저가 same-origin으로 보기에
      SOP를 만족한다. 즉, CORS로 접근하는 시나리오가 아니다.
      (즉 DELETE, PUT 등의 method들에 대해서도 preflight request를 하지 않는다.)
    • proxy에서 서버로 요청을 전달할 때는 cross-origin이지만,
      이미 브라우저의 영역에서 벗어났기 때문에 CORS와는 관계없다.

(이는 그저 우회해서 브라우저를 속일? 뿐이지, 쓸만하지만 제대로 된 해결 법은 아닌 듯 하다.
다만, CORS에 대해 별도의 설정이 없어 SOP만 적용되는 경우의 서버에 대해서는 용이한 듯 하다.
이런 서버로는 어떤 request를 하건 response에 Access-Control-Allow-Origin이 없어서
브라우저에서는 무조건 CORS에러를 발생 시킬 수 있기 때문이다.

또한 여기서, preflight request 방식에 OPTIONS가 왜 필요한 지에 대한 이유를 살펴볼 수 있다.
이런 CORS 설정이 되지 않은 서버에 DELETE, PUT 등의 method로 요청 시
OPTIONS로 origin을 확인을 하지 않는다고 한다면,
서버에서는 이미 해당 요청을 일단 처리는 다 해버리기 때문에 문제가 된다.
뒤늦게 서야 브라우저는 허용되지 않은 origin이란 걸 알고 CORS에러를 낼 것이다.)


서버 측에서 Access-Control-Allow-Origin 헤더 설정

  • 이 밖에 다른 관련 헤더도 추가 설정할 수 있다.
  • 특정 출처 혹은 모든 출처를 가능하게도 할 수 있다.
  • 보통 framework마다 Flask-cors와 같은 middleware가 마련되어 있다.
  • 클라이언트 측에서도 credential 같은 경우를 위해서는 헤더를 설정해야 한다.

0개의 댓글