cors

Yeongjong Kim·2021년 12월 22일
0

CORS(CROSS ORIGIN REQUEST SHARE)

이 포스트에서는 CORS가 무엇인지 알아보고, 예제를 통해 실습을 해보려고 한다.

CORS 란 무엇인가?

Cross Origin Request Share의 약어로 (교차 출처 요청 공유) 라고 부른다.
이 정책을 이해하기 위해서는 SOP(Same Origin Policy) 정책에 대해 알아볼 필요가 있다.
SOP는 어떠한 출처(Origin)에서 불러온 문서나 스크립트가 다른 출처(Origin)에서 가져온 리소스와 상호작용하는 것을 제한하는 보안 방식이다.
하지만, 다른 출처로 요청을 허용하는 예외가 있는데 그 것이 바로 CORS다.

SOP(SAME ORIGIN POLICY)

SOP는 동일 출처가 아니면 요청을 막는 정책이다. 그렇다면 동일 출처란 무엇인가? 출처(Origin)는 프로토콜, 호스트, 포트가 동일한 것을 Same Origin으로 판단하며, 이는 브라우저가 document.domain을 보고 판단한다.

동일 출처 정책에서 상호작용의 범주는 보통 쓰기, 삽입, 읽기 세가지 항목으로 나뉜다.

  1. 교차 출처 쓰기는 일반적으로 허용한다. link, redirect, submit등이 이 쓰기에 속한다.

    • 링크를 통해 다른 페이지로 이동하는 경우
    • redirect를 통해 다른 페이지로 이동하는 경우
      - meta 태그의 http-equiv를 refresh로 설정
      - window.location.replace(href)를 통해 페이지 이동을 하는 경우
      	즉 쓰기는 현재 문서가 다른 문서로 이동하는 경우를 의미한다.
  2. 교차 출처 삽입도 일반적으로 허용한다.

    • link 태그를 통해 stylesheets를 요청하는 경우
    • script 태그의 src 어트리뷰트로 자바스크립트 문서를 요청하는 경우 (단, Error details for syntax errors are only available for same-origin scripts.)
    • img, video, audio, object, embed 태그에 의한 리소스 요청
    • @font-face에 의한 폰트요청(하지만 일부 브라우저는 동일 출처를 요구할 수 도 있다.)
    • iframe에 의한 리소스 요청(이를 방지하려면 X-Frame-Options를 Header에 포함시켜야 한다.)
  3. 교차 출처 읽기는 일반적으로 불허한다. 하지만, 종종 교차 출처 삽입 과정에서 읽기에 대한 권한이 누출된다.(이미지의 크기, script 등)

    Jsonp

    3번의 읽기 권한 예시로 script 삽입에 의해 js 코드의 실행이 가능하다는 점을 원리로 우회가능한 방법이 바로 Jsonp다. 이 방법을 사용하면 XMLHttpRequest 방식으로 리소스를 요청하는 것이 아니라, script 태그에 의해 삽입되는 것과 같이 block문으로 인식하도록 동작한다. 자세한 내용은 wiki를 참고하자.

참고: https://ko.wikipedia.org/wiki/JSONP

3번 항목이 매우 중요하다. 교차 출처 읽기는 일반적으로 불허하기 때문에 이를 공식적으로 허용하는 방법은 CORS를 사용하는 것이다.

CORS protocol은 언제 적용되는가?

WebGL 텍스쳐, drawImage(), 이미지로부터 추출하는 CSS Shapes from images 등의 상황이 있지만, 개발자가 가장 흔히 마주치는 경우는 아래 두 가지이다.

  • XMLHttpRequest와 Fetch API 호출
  • Web Fonts(for cross-domain font usage in @font-face within CSS)

실습

express js로 간단한 백엔드 서버를 만들고 실습을 해보자.
3000번 포트를 통해 서버를 열고, json 요청 상황을 만들 것이다.

const express = require('express');

const app = express();
const port = 3000;

app.get('/users/:userID', (req, res) => {
  const { userID } = req.params;
  res.json({ userID });
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

3000번 외의 포트로 또 다른 서버를 열고 fetch를 사용하게 되면 다른 포트로 요청하기 때문에 SOP를 위반하는 상황을 만들 수 있다.

document.addEventListener('DOMContentLoaded', async () => {
  const data = await fetch('http://localhost:3000/users/tory');
});

콘솔을 열어보면 아래 이미지 처럼 에러 메세지가 출력되고, 친절하게 요청한 리소스에는 Access-Control-Allow-Origin 헤더가 없다는 것을 알려주고 있다.

reqeust, response headers 살펴보기

request header를 보면 여러가지 정보들이 포함되어 있다.

start line을 시작으로 Cache-Control, 브라우저가 해석가능한 압축방식 등이 있다.

여기서 가장 중요한 것은 Origin(출처)다.

CORS를 적용하기 위해서는 서버에서 보내주는 response header의 Access-Control-Allow-Origin에 현재 브라우저에서 요청을 보낸 출처가 포함되어있어야 한다.

하지만 Access-Control-Allow-Origin 가 보이지 않는다. 즉, 이 값은 개발자가 직접 지정해 주어야 한다는 뜻이다.

express.js에서 적용가능한 방법은 cors middleware를 사용하는 방법과 header를 직접 설정하는 방법이 있다.

middleware를 사용하면 모든 cors 요청에 cors와 관련된 옵션을 설정하여 header와 함께 응답하거나, 그 외에 함께 실행될 함수를 항상 실행시킬 수 있어 편리하다.

하지만 여기서는 간단하게 setHeader 메서드로 실습해보았다.

app.get('/users/:userID', (req, res) => {
  const { userID } = req.params;
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
  res.json({ userID });
});

위와 같이 작성할 경우 cors가 적용되어 무사히 값을 받아볼 수 있다.

pre-flight

위의 로직구조를 살펴보면 브라우저는 요청을 두번 보내는 방식으로 동작한다.
request header를 보면 origin과 함께 요청할 자료 타입 및 메서드에 대한 자료 등이 포함된다.

이는 비교적 가벼운 리소스로 예비요청을 보내고 서버가 허용하는 출처인지를 확인하여 브라우저 스스로 요청에 대한 보안을 강화하기 위한 방법이다.
위 이미지를 보면 예비 요청은 OPTIONS 라는 HTTP 메소드를 사용하여 요청하고 있음을 확인할 수 있다. 그 다음 메인 요청을 보낸다.

대부분의 경우 위와 같이 pre-flight(예비요청)을 보내지만, 브라우저가 예비요청이 필요없다고 판단하는 경우에는 단순 요청(Simple Request)을 하게된다.

단순요청

단순 요청은 단 한번의 요청과 응답으로 출처를 확인하며 데이터를 받는 과정이 포함된다.

조건은 다음과 같다.

  1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
  2. 브라우저가 자동으로 설정하는 헤더값 외에, 개발자가 설정하는 헤더가 CORS-safelisted request-header의 리스트 내에서 이루어져야 한다. 이 목록은 Accept, Accept-Language, Content-Language, Content-Type 등이 있으며 이하 링크에서 확인할 수 있다. (cors-safelisted-request-header)
  3. 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.

Credentialed Request

마지막으로 Header에 서로 신뢰할 수 있는 인증 정보를 담아 확인하는 방법이다. 조금 더 강화된 보안 정책을 적용하고 싶을 때 사용한다.

옵션 값설명
same-origin(default)같은 출처의 요청에서만 인증 정보를 담을 수 있다
include모든 요청에 인증 정보를 담을 수 있다
omit모든 요청에 인증 정보를 담지 않는다

include 값을 사용하여 인증 정보를 확인하거나, omit은 인증 정보 자체를 담지 못하도록 설정할 수 있다.

이 방식은 추후에 실제 사례를 접하게 된다면 업데이트 하도록 하겠다.

profile
Front 💔 End

0개의 댓글