포스트

CORS를 자세하게 설명할 수 있나요?

참고: 이 글은 MDN을 많이 참고하거나 인용하였고, 영어 용어는 임의로 번역했지만 처음 언급할 땐 원어를 같이 표기하였다.

웹 개발을 경험해 본 사람이라면 FE, BE를 막론하고 CORS 에러를 만나본 적이 있을 것이다. 나도 서버 개발을 하며 수없이 CORS 에러를 만나왔고, 그 때 마다 구글링을 통해 이 문제를 해결했다. 그러면서 얼핏 본 내용들을 통해 대략적으로는 이해하고 설명할 수 있었지만, 정확한 개념은 이해하지 못하고 있었다.

이 글을 쓰며 CORS를 정확하게 이해하고, 이 글을 읽는 분들도 CORS를 쉽고 정확하게 이해할 수 있길 바란다.

CORS가 왜 필요할까

일반적으로 교차된 출처 간 자원을 아무런 제약 없이 공유하는 것은 보안상의 심각한 위험이 있다. 예를 들어보자.

  1. 나는 은행 사이트(예: mybank.com)에서 로그인을 했고, 브라우저 쿠키에는 나의 인증 정보가 담겨있다.
  2. 그러고 나서 우연히 들어간 악성 사이트(예: hacker.com)에는 mybank.com에 나의 민감정보를 가져오는 요청을 보내는 악의적인 스크립트가 심겨 있었다.
  3. 이미 브라우저 쿠키에는 나의 인증 정보가 담겨 있었고, 해커는 그 스크립트를 통해 요청을 보낸 후 나의 민감정보 응답을 읽었다.

이 예에선 hacker.com의 스크립트가 다른 출처의 자원인 mybank.com을 제약 없이 읽을 수 있어서 보안 사고가 발생했다.

이러한 시나리오를 예방하기 위해 현대의 모든 브라우저는 Same-origin policy(SOP)를 통해 어떤 하나의 출처(origin)에서 로드된 문서나 스크립트가 다른 출처의 자원과 상호작용 하는 것을 엄격하게 제한하고 있다.

하지만 현대에 들어서며 프론트엔드 서버와 백엔드 서버가 분리되어 있거나, 다른 서비스의 API를 불러오는 등 서로 다른 출처의 자원에 접근할 일이 많아졌다. 이러한 상황에서 SOP를 유지하면서도 다른 출처 간의 데이터 접근을 할 수 있도록 완화하기 위해 만든 정책이 Cross-Origin Resource Sharing(CORS)이다.

CORS의 작동 방식

CORS는 몇 가지 새로운 HTTP 헤더를 추가하는 방법으로 보안을 유지한다. CORS가 작동하는 흐름은 몇 가지가 있는데 하나하나 자세하게 다뤄보겠다.

1. 간단한 요청 (Simple requests)

Simple request는 특정한 조건을 만족하는 간단한 요청을 의미한다. 그 특정한 조건은 아래와 같다.

모든 조건을 만족해야 함

  • HTTP METHOD가 다음 중 하나임: GET, HEAD, POST
  • 다음 헤더 외의 헤더를 임의로 설정하지 않았음:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (아래의 타입만 허용됨)
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
    • Range
  • XMLHttpRequest로 만든 요청의 경우 이벤트 리스너가 등록되어 있지 않았음
  • ReadableStream 개체를 사용하지 않았음

위 조건을 만족하는 간단한 요청의 경우 ‘preflight’를 통해 요청을 검증하는 과정(아래에서 자세히 다룬다)을 생략한다. 이러한 예외가 생기게 된 배경은, CORS 표준이 생기기 전 HTML에선 이미 <form>을 통해 교차된 출처에 요청을 자유롭게 보낼 수 있었고, 이에 따라 발생하는 취약점은 이미 방어하고 있다고 가정하기 때문이다.

요청이 전송되면 브라우저는 서버 응답의 Access-Control-Allow-Origin 헤더를 읽어 허용된 출처인지 검증 후 결과를 반환한다.

2. 사전 검증된 요청 (Preflighted requests)

사전 검증된 요청은 브라우저가 어떠한 요청을 실제로 다른 출처로 보내기 전에, ‘preflight’(이하, 사전 검증) 요청을 서버에 먼저 보내어 실제 요청이 안전한지 검증하는 과정을 거친다. 이해를 돕기 위해 흐름을 정리해보겠다.

1
2
3
4
1. 자바스크립트 코드에서 POST 요청 fetch() 함수 호출.
2. 브라우저는 실제 요청(POST)을 보내기 전, 헤더에 몇 가지 정보를 실어 OPTIONS 요청(사전 검증 요청)을 먼저 보냄.
3. 서버는 사전 검증 요청을 받고, 실제로 허용되는 헤더와 HTTP 메서드를 응답 헤더에 실어 반환.
4. 브라우저는 서버의 응답 헤더를 읽어서 실제 요청이 허용된다면 실제 요청을 전송하고 아니라면 에러를 뱉음

브라우저는 CORS 표준에 따라서 위 흐름처럼 교차 출처 요청을 안전하게 관리한다.

조금 더 자세하게 사전 검증 요청에 대해 알아보자. 아래는 브라우저가 보내는 사전 검증 요청의 예이다.

1
2
3
4
5
6
7
8
9
10
OPTIONS /doc 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
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother

이 예에서 눈여겨 볼 것은 Access-Control-Request-MethodAccess-Control-Request-Headers 헤더이다. 브라우저는 실제 요청의 HTTP Method를 Access-Control-Request-Method헤더에, 실제 요청의 헤더를 Access-Control-Request-Headers에 실어서 보낸다.

서버는 이 헤더가 담긴 OPTIONS 요청을 받았다면 다음과 같이 응답한다.

1
2
3
4
5
6
7
8
9
10
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

직관적으로 알 수 있듯이 Access-Control-Allow-* 헤더들은 실제로 서버가 허용하고 있는 출처, 메서드, 헤더를 의미한다. 브라우저는 이 응답을 읽고 실제 요청을 보낼 지 거부하고 에러를 낼지 판단한다.

3. 자격 증명이 포함된 요청 (Requests with credentials)

기본적으론 교차 출처 요청을 보낼 땐 쿠키나 HTTP 인증 등을 보내지 않는다. 자격 증명 정보를 담아서 보내고 싶다면 클라이언트, 서버 모두 명시적으로 자격 증명을 활성화해 줘야 한다.

클라이언트가 자격 증명을 활성화하여 요청을 보냈다면, 서버는 반드시 Access-Control-Allow-Credentials: true 헤더를 응답에 포함해야하며, Origin, Headers, Methods 등의 접근 제어 헤더에 와일드카드(*)가 포함되면 안된다. 만약 서버가 Access-Control-Allow-Credentials를 true로 설정하지 않았거나 접근 제어 헤더에 와일드 카드를 사용했다면, 브라우저는 콘솔에 오류를 띄우고 응답을 읽을 수 없게 된다.

이러한 엄격한 조건을 모두 만족해야만, 서로 다른 출처끼리 자원을 공유할 수 있다. 이 배경엔 자격 증명을 요하는 정보는 민감한 정보가 담겨있을 확률이 높기 때문일 것이다.

정리

글을 작성하며 CORS의 중요성을 느끼게 되었고, 웹에서 생기는 보안 취약점도 알게되었다. 이제 와일드카드를 사용하거나 그냥 CORS를 꺼버리는 등의 대처보다 CORS 에러를 정확히 이해하고 올바른 대응을 할 수 있을 것이다.

This post is licensed under CC BY 4.0 by the author.