본문 바로가기

그 외

[HTTP] CORS(Cross-Origin Resource Sharing) 이란

Cross-Origin Resource Sharing 은 추가 HTTP 헤더를 사용하여 브라우저가 한 출처에서 실행중인 웹 애플리케이션에 선택된 액세스 권한을 부여하도록하는 메커니즘입니다.

CORS 를 알기전에 same-origin policy 에 대해 알아야합니다.

   

 

 

Same-Origin Policy

same-origin policy 는 우리나라말로 동일 출처 정책으로, 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. same-origin policy 는 malware 나 위협적인 접근에 대한 문서를 분리해, 공격받을 수 있는 경로를 줄입니다.

same-origin 의 정의로는 두 URL 의 프로토콜, 포트(명시한 경우), 호스트가 모두 같아야 same-origin 이라고 말합니다.

즉, web application 은 본인으로부터 다른 origin (domain, protocol or port) 의 요청을 받았을 때 cross-origin HTTP request 를 실행합니다.

 

아래 표는 URL http://store.company.com/dir/page.html 의 origin 을 비교한 예시입니다.

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

브라우저들은 스크립트 내에서 초기화되는 cross-origin HTTP 요청을 제한합니다. 예를 들어, XMLHttpRequest 와 Fecth API 는 위에서 설명한 same-origin policy 를 따르기 때문에 다른 API 의 응답에 올바른 CORS 헤더가 포함되어 있지 않으면 해당 API 를 사용하는 웹 응용 프로그램은 응용 프로그램이 로드된 same-origin 의 resource 만 요청할 수 있습니다.

따라서, Spring Boot 내에서도 특정한 설정이 되어있지 않다면 CORS 오류가 발생하여 block 이 될 수 있기 때문에 same-origin 으로 통신하지 않는다면 CorsConfiguration 를 통해 cors 설정을 해주어야 합니다.

하지만, 모든 요청이 same-origin policy 를 따르는 것은 아닙니다.

CORS 요청의 종류로는

  • Simple Request
  • Preflight Request
  • Credential Request

로 총 3가지가 존재합니다.

순서대로 설명드리도록 하겠습니다.

     

Simple Request

특정 request 는 CORS preflight 를 trigger 하지 않습니다. 이러한 것을 simple requests 라고 합니다.

simple request 란 다음의 조건을 모두 충족시키는 request 입니다.

  1. 허용되는 Method
    • GET
    • HEAD
    • POST
  2. headers 를 자동으로 user agent 로 부터 분리시킵니다. (예를들어, Connection, User-Agent, 헤더에 정의된 "forbidden header name") 다음과 같은 Fetch spec 에 정의되어있는 CORS-safelisted request-header 헤더들만 허락해줍니다.
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • width
  3. Content-Type 의 헤더에는 다음과 같은 값만 허용됩니다.
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  4. XMLHttpRequestUpload 에 등록된 request 의 event listeners 객체는 모두 허용되지 않습니다. XMLHttpRequest.upload property 를 사용하여 접근할 수 있습니다.
  5. ReadableStream 은 request 에 사용할 수 없습니다.

이러한 종류의 cross-site request 는 이미 web content 에서 요청할 수 있습니다.

만약 그 server 가 적절하지 않은 헤더를 보낸다면 requester 에게 response data 를 보내지 않을겁니다.

그러므로, 위조된 cross-site request 을 막는 site 는 새로운 HTTP access control 에 대한 걱정할 필요가 없습니다.

다음은 simple request 요청에 대한 간단한 예시입니다.

GET /resources/public-data/ HTTP/1.1
  Host: bar.other
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-us,en;q=0.5
  Accept-Encoding: gzip,deflate
  Connection: keep-alive
  Origin: https://foo.example
  

request header 의 Origin 을 보시면, https://foo.example 에서 오는것을 알 수 있습니다.

HTTP/1.1 200 OK
  Date: Mon, 01 Dec 2008 00:23:53 GMT
  Server: Apache/2
  Access-Control-Allow-Origin: *
  Keep-Alive: timeout=2, max=100
  Connection: Keep-Alive
  Transfer-Encoding: chunked
  Content-Type: application/xml

  […XML Data…]
  

위의 응답에서, server는 Access-Control-Allow-Origin 헤더를 다시 보냈습니다. Origin 헤더의 사용과 Access-Control-Allow-Origin 의 사용은 acces control protocol 을 가장 간단하게 사용했다는 것을 보여줍니다.

또한, Access-Control-Allow-Origin: * 은 어떤 domain 이든지 resource 를 허락한다는 것을 의미합니다.

만약, 특정 resource 의 owners 에게만 허락하고 싶으면 다음과 같이 정의하시면 됩니다.

Access-Control-Allow-Origin: https://foo.example
  
     

Preflighted request

simple requeest 가 아니라면 preflighted request 가 실질적인 request 이 안전한지에 대한것을 결정하기 위해 OPTIONS method 를 사용하여 특정 domain 으로 HTTP request 를 우선 던집니다.

Preflight Request 는 OPTIONS Method 를 통해 한번 확인을 하기 떄문에 simple request 에 비하여 까다롭지 않습니다.

다음과 같은 조건을 가집니다.
  1. 모든 Method 를 허용합니다.
  2. application/json 이나 application/xml 등 다른 Content-type 으로 요청을 보낼 수 있습니다.
  3. Custom header 를 사용할 수 있습니다.

간단한 예시를 들어보겠습니다.

OPTIONS /resources/post-here/ HTTP/1.1
  Host: bar.other
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-us,en;q=0.5
  Accept-Encoding: gzip,deflate
  Connection: keep-alive
  Origin: http://foo.example
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: X-PINGOTHER, Content-Type


  HTTP/1.1 204 No Content
  Date: Mon, 01 Dec 2008 01:15:39 GMT
  Server: Apache/2
  Access-Control-Allow-Origin: https://foo.example
  Access-Control-Allow-Methods: POST, GET, OPTIONS
  Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  Access-Control-Max-Age: 86400
  Vary: Accept-Encoding, Origin
  Keep-Alive: timeout=2, max=100
  Connection: Keep-Alive
  

일단 위와 같은 OPTIONS 의 Method 로 preflight request 가 성공하면, 신뢰한다고 가정하여 다음과 같은 실제 request 를 보냅니다.

POST /resources/post-here/ HTTP/1.1
  Host: bar.other
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-us,en;q=0.5
  Accept-Encoding: gzip,deflate
  Connection: keep-alive
  X-PINGOTHER: pingpong
  Content-Type: text/xml; charset=UTF-8
  Referer: https://foo.example/examples/preflightInvocation.html
  Content-Length: 55
  Origin: https://foo.example
  Pragma: no-cache
  Cache-Control: no-cache

  <person><name>Arun</name></person>


  HTTP/1.1 200 OK
  Date: Mon, 01 Dec 2008 01:15:40 GMT
  Server: Apache/2
  Access-Control-Allow-Origin: https://foo.example
  Vary: Accept-Encoding, Origin
  Content-Encoding: gzip
  Content-Length: 235
  Keep-Alive: timeout=2, max=99
  Connection: Keep-Alive
  Content-Type: text/plain

  [Some GZIP'd payload]
  

Access-Control-Request-Method: POST

Access-Control-Request-Headers: X-PINGOTHER, Content-Type

Access-Control-Request-Method 헤더는 server 의 preflight request 부분인 실제 request 가 보내졌음을 나타내며 실제 POST Method request 입니다.

Access-Control-Request-Headers 는 server 가 실제 request 를 보낼 때 X-PINGOTHERContent-Type 의 Custom Headers 와 함께 보낸다고 알리는 것입니다.

Access-Control-Allow-Origin: [http://foo.example

Access-Control-Allow-Methods](http://foo.exampleAccess-Control-Allow-Methods): POST, GET, OPTIONS

Access-Control-Allow-Headers: X-PINGOTHER, Content-Type

Access-Control-Max-Age: 86400

Server 는 Access-Control-Allow-MethodsPOSTGET 이 가능한 Method 라고 헤더에 포함시켰습니다.

이러한 header 는 Allow response header 와 유사하지만, access control 의 context 에서는 엄격하게 사용해야 합니다.

server 는 또한 Access-Conrol-Allow-HeadersX-PINGOTHER, Content-Type 와 함께 보내며 이러한 실제 request 를 승인한다는 것을 나타내고 있습니다.

마지막으로, Access-Control-Max-Age 는 다른 preflight request 없이 얼마나 오래 해당 preflight request 에 대한 response cashe 할 수 있는지에 대한 두번째 값입니다.

위에 예시에서는, 86400 초인 24시간을 cashe 합니다.

또한, 각각의 browser 마다 최대 interval value 를 우선 가지고 있습니다.

대부분은 preflighted request 후에 redirect 를 동시에 지원을 하고 있지 않습니다.

만약, preflighted request 후에 redirect 가 발생하면, 대부분의 browser 는 다음과 같은 error message 를 보여줄 것입니다.

The request was redirected to 'https://example.com/foo', which is disallowed for cross-origin requests that require preflight

Request requires preflight, which is disallowed to follow cross-origin redirect

CORS protocol 은 기본적으로 지원을 했었는데, 추후에 지원하지 않도록 변경되었습니다.

그러나, 모든 브라우저가 그런 변경에대해 적용한 것은 아니며 기존의 방식을 사용하는 브라우저도 존재합니다.

따라서, 모든 브라우저가 표준이 되기전까지

     

Requests with credentials

Credential RequestXMLHttpRequest 를 통해 request 시 HTTP cookiesHTTP Authentication 를 인지하도록 해주는 것입니다.

기본설정으로는, browsers 가 credentials 를 보내지 않습니다. 따라서 withCredentials 를 true 를 설정하여 credentials request 를 요청할 수 있습니다.

아래는 credentials request 의 간단한 예시입니다.

GET /resources/access-control-with-credentials/ HTTP/1.1
  Host: bar.other
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-us,en;q=0.5
  Accept-Encoding: gzip,deflate
  Connection: keep-alive
  Referer: http://foo.example/examples/credential.html
  Origin: http://foo.example
  Cookie: pageAccess=2


  HTTP/1.1 200 OK
  Date: Mon, 01 Dec 2008 01:34:52 GMT
  Server: Apache/2
  Access-Control-Allow-Origin: https://foo.example
  Access-Control-Allow-Credentials: true
  Cache-Control: no-cache
  Pragma: no-cache
  Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
  Vary: Accept-Encoding, Origin
  Content-Encoding: gzip
  Content-Length: 106
  Keep-Alive: timeout=2, max=100
  Connection: Keep-Alive
  Content-Type: text/plain


  [text/plain payload]
  

위 내용을 보시면 10번째 라인에 Cookie 가 정의되어 있지만, 만약 bar.orhter 가 Access-Control-Allow-Credentials: true 의 헤더와 함께 response 하지 않으면 그 response 는 무시되고 web content 를 이용할 수 없게 만들어집니다.

credentialed request를 response 할 때, * 대신에 서버는 Access-Control-Allow-Origin header 에 특정 origin 을 꼭 명시해줘야 합니다.

그 이유는, Cookie header 를 포함한 request 가 만약 header 에 Access-Control-Allow-Origin* 라면 실패할 것이기 때문입니다.

 

 

 

총 3가지의 Requeest 요청에 대해 알아보았습니다.

처음 웹개발을 진행할 때 CORS 에 대해 알지 못해, front 에서 api 서버로 통신이 되지 않아 몇일간 해맸던 기억이 있습니다.

그 당시에는 이런 정보를 알지 못해 다른 것들만 이것저것 수정하다가 CORS 가 원인인 것을 알고 중요성을 느껴 찾아봤던 기억을 토대로 작성하였습니다.

만약 CORS 관련하여 서버에 오류가 발생한다면 ( OPTIONS Method 에 대한 오류나, XMLHttpRequest 관련 오류) 위의 해당 내용을 숙지하여 개발하면 쉽게 해결하실 수 있을 겁니다.

CORSCross-Origin 문제를 해결하는 방법이기도 하지만, 보안적으로도 가져가야할 규약이기도합니다.

따라서 신중한 선택과 고민을 통해 HTTP hedaer 와 허용 가능한 Method 를 선택할 필요가 있습니다.

 

 

 

 

REFRENCE

 - https://homoefficio.github.io/2015/07/21/Cross-Origin-Resource-Sharing/

 

Cross Origin Resource Sharing - CORS

개요HTTP 요청은 기본적으로 Cross-Site HTTP Requests가 가능하다. 다시 말하면,

태그로 다른 도메인의 이미지 파일을 가져오거나, 태그로 다른 도메인의 CSS를 가져오거나,

 

 - https://developer.mozilla.org/ko/docs/Web/HTTP/Access_control_CORS

 

HTTP 접근 제어 (CORS)

cross-origin request의 예: https://domain-a.com에서 제공되는 프론트 엔드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json를 요청.

developer.mozilla.org

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