본문 바로가기
FrontEnd

CORS(Cross-Origin Resource Sharing) 문제 해결하기

by 푸고배 2022. 1. 6.

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)

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

 

예시: 

프론트 엔드(https://domain-a.com)의 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우, 보안 상의 이유로 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한한다. 예를 들어, XMLHttpRequest와 Fetch API동일 출처 정책(same-origin policy)을 따른다.

 

즉, 이 API를 사용하는 웹 애플리케이션은 자신의 Origin와 동일한 리소스만 불러올 수 있으며, 다른 Origin의 리소스를 불러오려면 그 Origin에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.

 

CORS 체제는 브라우저와 서버 간의 안전한 교차 출처 요청 및 데이터 전송을 지원한다. 최신 브라우저는 XMLHttpRequest 또는 Fetch와 같은 API에서 CORS를 사용하여 교차 출처 HTTP 요청의 위험을 완화한다.

 

동작 방식

교차 출처 리소스 공유 표준은, 브라우저가 해당 정보를 읽는 것을 허용할 오리진 정보를 담은 새로운 HTTP 헤더를 서버에 추가함으로써 동작한다. 추가적으로, 서버 데이터에 부수효과(side effect)를 일으킬 수 있는 HTTP 요청메서드(GET을 제외한 HTTP메서드)에 대해, CORS 명세는 브라우저가 요청을 OPTIONS 메서드로 "프리플라이트(prefilght, 사전전달)하여 지원하는 메서드를 요청하고, 서버의 허가가 떨어지면 실제 요청을 보내도록 요구하고 있다. 또한, 서버는 클라이언트에게 요청에 "인증정보(쿠키, HTTP 인증)을 함께 보내야 한다고 알려줄 수도 있다.

 

접근 제어 시나리오 예제

교차 출처 리소스 공유가 동작하는 방식을 보여주는 세 가지 시나리오를 설명한다. 모든 예제는 지원하는 브라우저에서 교차 출처 요청을 생성할 수 있는 XMLHttpRequest를 사용한다.

 

단순 요청(Simple Requests)

일부 요청은 CORS preflight를 트리거하지 않는다. "Simple Requests"는 다음 조건을 모두 충족하는 요청이다.

  • 다음중 하나의 메서드 : GET, HEAD, POST
  • 유저 에이전트가 자동으로 설정한 헤더
    • ex) connection, User-Agent(en-US), Fetch 명세에서 "forbidden header name"으로 정의한 헤더, Fetch 명세에서 "CORS-safelisted request-header"로 정의한 헤더(수동으로 설정 가능)
    • Accept, Accept-Language, Content-Language, Content-Type
  • Content-Type 헤더는 다음 값들만 허용
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

 

예를들어, https://foo.example의 웹 컨텐츠가 https://bar.other 도메인의 컨텐츠를 호출하려할 때, foo.example의 자바스크립트에 아래와 같은 코드가 사용될 수 있다. 

const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

클라이언트와 서버간에 간단한 통신을 하고, CORS 헤더를 사용해 권한을 처리한다.

브라우저의 Request Header (Origin을 보면 https://foo.example로부터 요청이 왔다는 것을 확인 가능)

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

서버로부터의 response Header

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…]

Access-Control-Allow-Origin 헤더를 다시 전송한다.

가장 간단한 접근 제어 프로토콜은 Origin 헤더와 Access-Control-Allow-Origin을 사용하는 것이다.

이 경우 서버는 Access-Control-Allow-Origin:* 으로 응답해야 하며, 이는 모든 도메인에서 접근할 수 있음을 의미한다.

만일 https://foo.example의 요청만 리소스에 대한 접근을 허용하려면 Access-Control-Allow-Origin을 아래와 같이 수정한다.

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

 

프리플라이트 요청(Preflighted Request)

위에서 논의한 "Simple Requests" 와는 달리, 먼저 OPTIONS 매서드를 통해 다른 도메인의 리소스로 HTTP 요청을 보내 실제 요청이 전송하기에 안전한지 확인한다. Cross-site 요청은 유저 데이터에 영향을 줄 수 있기 때문에 이와 같이 사전 전송한다.

 

아래는 프리플라이트 할 요청의 예제이다.

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('Ping-Other', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');

POST 요청과 함께 보낼 XML body를 만든다. 또한 비표준 HTTP ping-Other 요청 헤더가 설정된다. 이러한 헤더는 HTTP/1.1의 일부가 아니지만 일반적으로 웹 응용 프로그램에 유용하다. Content-Type이 application/xml 이고, 사용자 정의 헤더가 설정되었기 때문에 이 요청은 프리플라이트 처리된다.

프리플라이트의 Request/Response 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는 서버에서 추가 정보를 판별하는데 사용하는 HTTP/1.1 메서드이다. 또한 safe 메서드이기 때문에, 리소스를 변경하는데 사용할 수 없다. OPTIONS 요청과 함께 두 개의 다른 요청 헤더가 전송된다..

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

Access-Control-Request-Method 헤더는 preflight requeset의 일부로, 실제 요청을 전송할 때 POST 매서드로 전송된다는 것을 알려준다.

Access-Control-Request-Headers 헤더는 실제 요청을 전송할 때 X-PINGOTHER와 Content-Type 사용자 정의 헤더와 함께 전송된다는 것을 서버에 알려준다. 서버는 이 정보를 확인 후 요청을 수락할 지 결정할 수 있다.

 

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

위와 같이 서버의 response를 보면 서버는 Access-Control-Allow-Methods로 응답하고 POST와 GET이 리소스를 쿼리하는데 유용한 메서드라고 가르쳐준다. 이 헤더는 Allow 응답 헤더와 유사하지만, 접근 제어 컨텍스트 내에서 엄격하게 사용된다.

또한 Access-Control-Allow-Headers의 값을 "X-PINGOTHER, Content-Type"으로 전송하여 실제 요청에 헤더를 사용할 수 있음을 확인한다. Access-Control-Allow-Methods와 마찬가지로 Access-Control-Allow-Headers는 쉼표로 구분된 허용 가능한 헤더 목록이다.

 

실제 Request/Response Header

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]

 

Prefilghted Request와 리다이렉트

모든 브라우저가 preflighted request 후 리다이렉트를 지원하지는 않는다.

prefilghted request 후 리다이렉트가 발생하면 일부 브라우저는 다음과 같은 오류 메시지를 띄운다.

  • 요청이 'https://example.com/foo'로 리다이렉트 되었으며, preflight가 필요한 cross-origin 요청은 허용되지 않습니다.
  • 요청에 preflight가 필요합니다. preflight는 cross-origin 리다이렉트를 허용하지 않습니다.

CORS 프로토콜은 원래는 리다이렉트가 필요했지만, 지금은 리다이렉트가 필요하지 않도록 변경되었다. 하지만 모든 브라우저가 변경되지는 않았기 때문에 리다이렉션은 여전이 나타난다. 아래와 같은 방법으로 변경되지 않응 브라우저에도 제한을 해결할 수 있다.

  • preflight 리다이렉트를 방지하기 위해 서버 측 동작을 변경
  • preflight를 발생시키지 않는 simple request가 되도록 요청을 변경

그러나 요청에 Authrization 헤더가 있기 때문에 preflight를 트리거하는 요청일 경우, 위와 같은 방법으로 제한을 제거할 수 없다. 또한, 요청이 있는 서버를 제어하지 안으면 문제를 해결할 수 없다.

 

인증정보를 포함한 요청

XMLHttpRequest 혹은 Fetch를 사용할 때 CORS에 의해 드러나는 가장 흥미로운 기능은 "credentialed" requests이다. credentialed requests는 HTTP Cookies와 HTTP Authentication 정보를 인식한다. 기본적으로 cross-site XMLThhpRequest나 Fetch 호출에서 브라우저는 자격 증명을 보내지 않는다. XMLHttpRequest 객체나 Request 생성자가 호출될 때 특정 플래그를 설정해야 한다.

 

이 예제에서 원래 http://foo.example에서 불러온 컨텐츠는 쿠키를 설정하는 http://bar.other 리소스에 simple GET request를 작성한다. foo.example의 내용은 다음과 같은 자바스크립트를 포함할 수 있다. 

const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';

function callOtherDomain() {
  if (invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send();
  }
}

withCredentials = true는 쿠키와 함께 호출하기 위한 XMLHttpRequest의 플래그이다. 기본적으로 호출은 쿠키없이 이루어지며, simple GET request이기 때문에 preflighted 되지 않는다. 그러나 브라우저는 Access-Control-Allow-Credentials:true 헤더가 없는 응답을 거부한다. 따라서 호출된 컨텐츠에 응답을 제공하지 않는다.

클라우드와 서버간의 통신 예제는 아래와 같다.

GET /resources/credentialed-content/ 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]

request에는 http://bar.other의 컨텐츠를 대상으로 하는 쿠키가 포함되어 있으나, response에 Access-Control-Allow-Credentials:true로 응답하지 않으면, 응답은 무시되고 웹 컨텐츠는 제공되지 않는다.

 

실행 전 요청 및 자격 증명

CORS 실행 전 요청에는 자격 중명이 포함되지 않아야 한다. 실행 전 요청에 대한 응답은 Access-Control-Allow-Credentials:true를 지정하여 자격증명으로 실제 요청을 수행할 수 있음을 나타내야 한다.

 

자격증명 요청 및 와일드카드(Credentialed requests and wildcards)

자격 증명 요청에 응답할 때 서버는 Access-Control-Allow-Origin에 반드시 "*"(와일드 카드)를 지정하는 대신 특정 Origin을 지정해야한다. Cookie 헤더가 포함된 경우(Access-Control-Allow-Credentials:true) Access-Control-Allow-Origin:*이면 요청에 실패한다. 

 

Third-party Cookies

CORS Response에 설정된 쿠키에는 일반적으로 third-party cookie 정책이 적용된다. 때문에 사용자의 브라우저 설정이 모든 third-party cookies를 거부하도록 되어있으면 이 쿠키는 저장되지 않는다.

 

 

HTTP Request Header

Origin : <origin>

cross-site 접근 요청 또는 preflight request의 오리진을 나타낸다.

접근 제어 요청에는 항상 Origin 헤더가 전송된다.

 

Access-Control-Request-Method: <method>

실제 요청에서 어떤 HTTP 메서드를 사용할지 서버에 알려주기 위해, preflight request 할 때 사용된다.

 

Access-Control-Request-Headers: <field-name>[, <field-name>]*

실제 요청에서 어떤 HTTP 헤더를 사용할지 서버에게 알려주기 위해, preflight request할 때 사용된다.

 

HTTP Response Header

Access-Control-Allow-Origin : <origin> | *

단일 오리진를 지정해 브라우저가 해당 오리진 리소스테 접근하는 것을 허용한다.

자격 증명이 없는 요청의 경우 "*" 와일드 카드는 브라우저의 오리진에 상관없이 모든 리소스에 접근하도록 허용한다.

 

Access-Control-Expose-Headers: <header-name>[, <header-name>]*

브라우저가 접근할 수 있는 헤더를 서버의 화이트리스트에 추가할 수 있다.

 

Access-Control-Max-Age : <delta-seconds>

preflight reqest 요청 결과를 캐시할 수 있는 시간을 나타낸다.

 

Access-Control-Allow-Credentials : true

credentials 플래그가 true일 때 요청에 대한 응답을 표시할 수 있는지를 나타낸다.

preflight request에 대한 응답의 일부로 사용하는 경우, credentials을 사용하여 실제 요청을 수행할 수 있는지를 나타낸다.

simple GET Requests는 preflighted되지 않으므로 credentials이 있는 리소스를 요청하면, 이 헤더가 리소스와 함께 반환되지 않는다.

이 헤더가 없으면 브라우저에서 응답을 무시하고 웹 컨텐츠로 반환이 안된다는 점을 주의한다.

 

Access-Control-Allow-Methods: <method>[, <method>]*

리소스에 접근할 때 허용되는 메서드를 지정한다.

이 헤더는 preflight request에 대한 응답으로 사용된다.

 

Access-Control-Allow-Headers: <header-name>[, <header-name>]*

preflight request에 대한 응답 헤더로 사용되며, 실제 요청 시 사용할 수 있는 HTTP헤더를 나타낸다.

 


Refrence

 

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

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

developer.mozilla.org

 

반응형

댓글