CORS

박재현·2022년 2월 8일
0
post-custom-banner

💡 CORS란?

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

  • 여기서 origin(출처) 이란 scheme(protocol), host(domain), port 로 구성

    예를들어 https://www.google.com/maps 라는 주소가 존재한다면
    protocol: https://
    Host: www.google.com
    Port: 443
    동일 출처(Same Origin)란 protocol, host, port 가 모두 같을때를 말한다.


💡 CORS를 사용하는 이유?

👉 다른 출처의 어플리케이션이 서로 통신하는 것에 대해 아무런 제약도 존재하지 않는다면 악의를 가진 사용자가 소스 코드를 보고 CSRF(Cross-Site Request Forgery)XSS(Cross-Site Scripting) 와 같은 방법을 사용하여 정보를 탈취할 수 있다.

👉 브라우저에서 다른 서버로 리소스를 요청할 경우 SOP를 우회하기 위한 여러가지 방법 중 가장 권장되는 방법

  • SOP(Same-Origin Policy): 같은 출처에서만 리소스를 공유할 수 있다는 규칙


CORS request와 response

💡 CORS response

CORS 요청이 온다면, 서버는 Access-Control-* 헤더를 메시지에 포함시킬 수 있습니다.

  • Access-Control-Allow-Origin : 요청을 허용할 출처를 명시할 때 사용하며, *를 사용하면 모든 출처의 리소스 요청을 허용합니다.
  • Access-Control-Allow-Methods : 어떤 메서드를 허용할 것인지 명시합니다.
  • Access-Control-Allow-Headers : 어떤 헤더들을 허용할 것인지 명시합니다.
  • Access-Control-Max-Age : preflight 요청에 대한 응답을 브라우저에서 얼마만큼 캐싱하고 있을지 설정할 때 사용합니다.
  • Access-Control-Expose-Headers : 브라우저가 스크립트에 노출시킬 헤더의 목록을 명시할 때 사용합니다. 기본적으로 다음 7가지 헤더(일명 CORS safe-listed header)는 따로 설정하지 않아도 노출시킵니다.
    Cache-Control
    Content-Language
    Content-Length
    Content-Type
    Expires
    Last-Modified
    Pragma
    Access-Control-Allow-Credentials : 추후 설명

서버는 위와 같은 헤더를 통해 리소스에 대한 CORS 요청을 어느 수준까지 허용할 것인지 클라이언트(브라우저)에게 알려주고, 이를 바탕으로 브라우저는 추후동작을 결정합니다.

💡 CORS request

  • CORS 요청을 보낼 때, 브라우저는 해당 요청을 두 가지 방식으로 처리합니다. Preflight request을 사용한 방식과 그렇지 않는 방식(Simple request)입니다.

🔎 Simple request

다음과 같은 조건을 만족하면, 브라우저는 CORS 요청을 Simple request로 처리합니다.

  • GET POST HEAD
  • CORS safe-listed request header을 사용하였을 때
  • Content-Type의 헤더 값으로 다음과 같은 값을 사용하였을 때
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • XMLHttpRequestUpload 객체에 이벤트 리스너가 등록되지 않은 경우
  • 요청에서 ReadableStream 객체를 사용하지 않은 경우

이 방식은 일반 요청이랑 동일하게 동작하며, 단 한가지 다른 점은 응답의 Access-Control-Allow-Origin에 따라 브라우저가 응답을 정상적으로 처리할지를 결정합니다.

예를 들어 http://localhost:8080에서 서버(http://localhost:9000/api/menus)에 요청을 보낸다면, 다음과 같은 요청 메시지가 전송됩니다. 간단하게 Origin 헤더만 추가해서 보내줍니다.

GET /api/menus HTTP/1.1

Host: localhost:9000
Referer: http://localhost:8080/
Origin: http://localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
...

아래 응답에 Access-Control-Allow-Origin 헤더가 존재하지 않기 때문에 브라우저는 이 응답을 자바스크립트 코드에 노출시키지 않고 오류를 뱉어냅니다.

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8
Content-Length: 155
...

교차 출처 요청 차단: 동일 출처 정책으로 인해 http://localhost:9000/api/menus에 있는 원격 자원을 차단하였습니다. (원인: ‘Access-Control-Allow-Origin’ CORS 헤더가 없음).

서버에 Access-Control-Allow-Origin 헤더를 추가해서 응답하면 다음과 같이 정상적으로 자바스크립트 코드에서 응답을 처리할 수 있습니다.

HTTP/1.1 200 OK

Access-Control-Allow-Origin: http://localhost:8080
Content-Type: application/json; charset=utf-8
Content-Length: 155
...

🔎 Preflight Request

1. 프리플라이트 요청 (Preflight Request)

  • Simple request가 아니면 브라우저는 실제 요청을 보내기 전에 한 가지 작업을 수행합니다. preflight request를 실제 요청 메시지보다 서버에 먼저 보냅니다. 서버는 이에 대한 응답을 주고, 브라우저는 이를 통해 실제 요청을 보낼지 결정합니다.

  • Cross-origin 요청은 유저 데이터에 영향을 줄 수 있기 때문에 Preflight 요청을 한다.

2. preflight request/response 동작

OPTIONS 요청과 함께 두 개의 다른 요청 헤더가 전송된다.
아래에서 첫 행은 실제 요청을 전송할 때 POST 메서드로 전송된다는 것이고, 두번째 행은 실제 요청을 전송 할 때 X-PINGOTHER 와 Content-Type 사용자 정의 헤더와 함께 전송된다는 것을 서버에 알려준다.

2.1)preflight response

  • Access-Control-Request-Method : 실제 요청에서 사용하는 메서드를 서버가 알 수 있도록 설정하는 헤더
  • Access-Control-Request-Headers : 실제 요청에 포함될 헤더를 서버가 알 수 있도록 설정하는 헤더
  • Origin : 요청을 보낸 출처로, URL 중 scheme과 host, port만 명시
Access-Control-Request-Method: POST	// 실제요청의 메서드
Access-Control-Request-Headers: X-PINGOTHER, Content-Type // 실제요청의 추가헤더
OPTIONS /api/menus HTTP/1.1

Host: localhost:9000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Referer: http://localhost:8080/
Origin: http://localhost:8080
...

2.2) preflight response

서버가 메서드와 헤더를 받을 수 있음을 알려준다. 마지막행은 preflight request에 대한 응답을 캐시할 수 있는 시간(초)이다. 아래에서는 86400초 (=24시간)

프리플라이트를 보내면 사전, 실제 요청 두번이 매번 왔다갔다 하므로 브라우저가 캐싱을 해두고 똑같은 요청을 보낼때, 사전 요청을 보내지 않고 바로 본 요청을 보낸다.

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 // Prefilght 응답 캐시기간
HTTP/1.1 200 OK

Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: -1
Connection: keep-alive
Content-Length: 0

preflight request가 완료되면 실제 요청을 전송

응답 성공

GET /api/menus HTTP/1.1

Host: localhost:9000
Referer: http://localhost:8080/
Content-Type: application/json
Origin: http://localhost:8080
...

응답 실패

HTTP/1.1 200 OK

Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Max-Age: -1
Connection: keep-alive
Content-Length: 0

실패 시 메세지

  • 교차 출처 요청 차단: 동일 출처 정책으로 인해 http://localhost:9000/api/menus에 있는 원격 자원을 차단하였습니다. (원인: CORS 사전 점검 응답의 헤더 ‘Access-Control-Allow-Headers’에 따라 헤더 ‘content-type’가 허용되지 않음)

  • 교차 출처 요청 차단: 동일 출처 정책으로 인해 http://localhost:9000/api/menus에 있는 원격 자원을 차단하였습니다. (원인: CORS 요청이 성공하지 못함)


🔎 인증정보 포함 요청 (Credentialed Request)

Access-Control-Allow-Credentials

CORS는 기본적으로 보안상의 이유로 쿠키를 요청으로 보낼 수 없도록 막고 있습니다.
하지만 다른 도메인을 가진 API 서버에 자신을 인증해야 정상적인 응답을 받을 수 있는 상황에서는 쿠키를 통한 인증이 필요합니다.

  • 인증 관련 헤더를 포함할 때 사용하는 요청이다.
  • 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 기본적으로 요청에 담지 않으므로, credentials 옵션을 변경하지 않고서는 cookie를 주고 받을 수 없다.

📣 브라우저 요청 시 설정 옵션
✓ omit : 절대로 cookie 들을 전송하거나 받지 않는다.
✓ same-origin : 동일 출처(same origin)이라면, user credentials (cookies, basic http auth 등..)을 전송한다. (default 값)
✓ include : cross-origin 호출이라 할지라도 언제나 user credentials (cookies, basic http auth 등..)을 전송한다.

예시)

fetch('주소', {
 credentials: 'include', // 모든 요청에 인증 정보 포함
});

// axios 로 통신할 시, withCredentials 설정을 true 로 넣어주면 된다.
axios.post(주소, 데이터, { withCredentials: true });

// 또는 공통으로 추가
axios.defaults.withCredentials = true;

또한 credentials 설정을 include/true 로 설정하면 CORS정책에 의해 Access-Control-Allow-Origin을 모든 출처를 허용하는 '' 로 지정할 수 없다는 에러가 발생하며, 따라서 cors 설정에서 을 입력하여 모든 출처를 허용한 경우에는 특정 출처를 정확히 명시해야 한다.

클라이언트

  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://example.com/', true);
  xhr.withCredentials = true;
  xhr.send(null);

서버

	Access-Control-Allow-Credentials: true

주의 🚨!

1. Access-Control-Allow-Origin은 여러 출처를 명시할 수 없다!

  • 스펙상 Access-Control-Allow-Origin은 하나의 origin이나 , null만 가질 수 있습니다. 그렇기 때문에 서브 도메인의 접근 허용을 위해 .example.com로 헤더 값을 설정하는 것은 스펙상 맞지 않습니다.

그래서 여러 출처의 접근 허용을 위해 값을 쓰는 경우가 많습니다. 하지만 는 모든 출처의 접근을 허용하는 것이기 때문에 보안상 좋지 않습니다. 그렇다면 여러 출처를 허용하고 싶다면, 어떤 방식을 취하는 것이 좋을까요?

2. Origin 헤더를 이용한 Whitelist 관리

요청이 오면 CORS 요청 헤더의 Origin 값을 확인하여, 별도로 관리하고 있는 Whitelist에 포함되고 있는지 확인하면 됩니다.

만약 존재한다면, Access-Control-Allow-Origin헤더의 값으로 해당 Origin의 값을 넣어 응답할 것이고, 브라우저는 실제 요청을 서버에 보낼 것입니다. 악의적인 사이트에서 요청을 보냈더라도, Whitelist에 포함되어 있지 않기 때문에 안전하게 CORS 요청을 처리할 수 있습니다.

CORS의 Whitelist를 직접 처리할 수도 있지만, cors 미들웨어가 이미 지원하고 있기 때문에 다음과 같이 작성하면 Whitelist를 편하게 관리할 수 있습니다.

// express cors
// https://www.npmjs.com/package/cors#configuration-options
app.use(
  cors({
    origin: ["http://localhost:8080", "http://localhost:7070"],
  })
);

3. Access-Control-Allow-Origin의 값에 scheme과 port는 꼭 명시되어야 한다!

Access-Control-Allow-Origin 헤더 값을 설정할 때, 자주 하는 실수 중 하나가 host 이름만 명시하는 경우입니다. www.test.com과 같이 scheme(protocol)를 생략하거나 포트번호가 80번이 아닌데도 포트번호를 생략한 경우입니다.

Accecc-Control-Allow-Origin의 헤더 값은 origin 스펙을 만족하는 값이기 때문에 [scheme]://[host]:[port] 형태로 명시해야 합니다. origin 스펙을 준수하지 않으면, 브라우저 단에서 Origin 헤더의 값과 비교를 잘못할 수 있기 때문에 꼭 origin 스펙을 준수해야 합니다.

4. Access-Control-Allow-Origin: *와 Access-Control-Allow-Credentials: true는 함께 사용할 수 없다.

CORS는 응답이 Access-Control-Allow-Credentials: true 을 가질 경우, Access-Controll-Allow-Origin의 값으로 *를 사용하지 못하게 막고 있습니다.

Access-Control-Allow-Credentials: true를 사용하는 경우는 사용자 인증이 필요한 리소스 접근이 필요한 경우인데, 만약 Access-Control-Allow-Origin: *를 허용한다면, CSRF 공격에 매우 취약해져 악의적인 사용자가 인증이 필요한 리소스를 마음대로 접근할 수 있습니다. 그렇기 때문에 CORS 정책에서 아예 동작하지 않도록 막아버린 것입니다.

Access-Contorl-Allow-Credentials: true인 경우에는 반드시 Access-Control-Allow-Origin의 값이 하나의 origin 값으로 명시되어 있어야 정상적으로 동작합니다.


출처

post-custom-banner

0개의 댓글