[WEB Server] CORS

jeyoon·2021년 5월 27일
0

brower security

Q. 브라우저가 항상 보안 위협을 받는 근본적인 이유는?
➡️ A. 자바스크립트를 구동하기 때문! 자바스크립트로 할 수 있는 것들이 많기 때문이다.

브라우저에서 자바스크립트로 할 수 있는 것들
1. ajax call을 해서 api를 호출할 수 있다
2. 다이나믹하게 DOM을 제어할 수 있다
3. 인증 정보를 브라우저에 저장할 수 있다
4. 인증 정보를 불러올 수도 있다

Q. 어떤 공격이 일어날 수 있을까?
➡️ A. 대표적인 두 가지: XSS, CSRF

XSS

클라이언트가 서버를 신뢰하기 때문에 발생하는 이슈

브라우저에서 서버에 메세지 요청시 해킹 관련 코드가 침투된다면? (<script /> 코드 )
그에 관한 응답을 받아 브라우저가 렌더하게 된다! ➡️ 해킹 완.

  • XSS 란, 보안이 약한 웹 어플리케이션에 대한 웹 기반 공격
  • XSS 공격의 희생자는 어플리케이션이 아닌 user
  • XSS 공격에서 해로운 컨텐츠는 javascript를 활용하여 전달됨

CSRF

반대로 서버가 클라이언트를 신뢰해서 발생하는 이슈

  • 서버는 클라이언트가 인증정보를 가지고 오면 믿는다.
  • 사용자가 인증 정보를 가진 채로 해커의 링크를 누르면, 해커는 인증 정보를 가로채서 서버에 요청을 보내버린다. (회원 탈퇴 등)

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


클라이언트의 도메인과 같은 도메인의 서버로부터 리소스를 받아오려고 할 때는 문제가 없다.
하지만 다른 도메인의 서버로부터 리소스를 받아오고 싶다면?? ➡️ 이때 필요한 것이 CORS 이다!

MDN 뜯어보기❗️
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS (영문)
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS (한글)
⬇️

  • CORS는 HTTP 헤더 기반 메커니즘
  • 서버는 브라우저가 "리소스 로드를 허용해야 하는 origin"이 아닌 "다른 origin(domain, scheme, or port)"을 나타낼 수 있도록 CORS HTTP 요청을 실행한다.
  • CORS는 또한 서버가 실제 요청을 허용하는지 확인하기 위해 "브라우저가 Cross-Origin Resource를 호스팅하는 서버에 preflight 요청을 하는 메커니즘"에 의존한다. preflight 요청에서 브라우저는 "HTTP 메서드를 나타내는 헤더"와 "실제 요청에 사용될 헤더"를 보낸다.

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

보안상의 이유로, 브라우저는 스크립트에서 시작한 cross-origin HTTP 요청을 제한한다. 예를 들어 XMLHttpRequest 및 Fetch API는 same-origin policy를 따른다. 즉, 이 API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 줘야 한다.

CORS 메커니즘은 브라우저와 서버 간의 안전한 cross-origin 요청 및 데이터 전송을 지원한다. 최신 브라우저는 XMLHttpRequest 또는 fetch와 같은 API에서 CORS를 사용하여 cross-origin HTTP 요청의 위험을 완화한다.

Functional overview

  • Cross-Origin Resource Sharing 표준은 새로운 HTTP 헤더를 추가함으로써 작동한다.
    • 새로운 HTTP 헤더란? ➡️ "웹 브라우저로부터 온 정보를 읽는 것이 허용된 origin"을 서버에서 설명하는 HTTP 헤더
  • 또한 서버 데이터에 side effect를 일으킬 수있는 HTTP 요청 메서드 (GET 이외의 HTTP 메서드)에 대해, CORS specification은
    • 브라우저가 요청을 OPTIONS 메서드로 "preflight(사전 전달)"하여 서버에서 지원되는 메서드를 요청하고,
    • 서버의 "허가"가 떨어지면 실제 요청을 보내도록 요구하고 있다.
  • 또한 서버는 클라이언트에게 요청에 "인증정보"(쿠키, HTTP 인증)를 함께 보내야 한다고 알려줄 수도 있다.
  • CORS 실패는 오류의 원인이지만, 보안상의 이유로 JavaScript에서는 오류의 상세 정보에 접근할 수 없으며, 알 수 있는 것은 오류가 발생했다는 사실 뿐이다. 정확히 어떤 것이 실패했는지 알아내려면 브라우저의 콘솔을 봐야 한다.

이후 항목에서는 시나리오와 함께, 사용한 HTTP 헤더의 상세 내용을 다룬다.

Examples of access control scenarios

Cross-Origin Resource Sharing이 동작하는 방식을 보여주는 세 가지 시나리오를 제시한다. 모든 예제는 지원하는 브라우저에서 CORS를 생성할 수 있는 XMLHttpRequest를 사용한다.

Simple requests (단순 요청)

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

  • 다음 중 하나의 메서드
    • GET
    • HEAD
    • POST
  • user agent가 자동으로 설정한 헤더(예를 들어, Connection, User-Agent, Fetch 명세에서 “forbidden header name”으로 정의한 헤더)를 제외하고, 다음의 헤더들만 사용 가능
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (아래의 추가 요구사항에 주의)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 헤더는 다음의 값들만 허용된다.
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 요청에 사용된 XMLHttpRequestUpload 객체에는 이벤트 리스너가 등록되어 있지 않다. 이들은 XMLHttpRequest.upload 프로퍼티를 사용하여 접근한다.
  • 요청에 ReadableStream 객체가 사용되지 않는다.

예를 들어, 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 헤더를 사용하여 권한을 처리한다.

이 경우 브라우저가 서버로 전송하는 내용을 살펴보고, 서버의 응답을 확인해보자.

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

요청 헤더의 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…]

서버는 이에 대한 응답으로 Access-Control-Allow-Origin 헤더를 다시 전송한다.
이와 같이 가장 간단한 접근 제어 프로토콜은 Origin 헤더와 Access-Control-Allow-Origin 을 사용하는 것이다.
이 경우 서버는 Access-Control-Allow-Origin: * 으로 응답해야 하며, 이는 모든 도메인에서 접근할 수 있음을 의미한다. https://bar.other의 리소스 소유자가 오직 https://foo.example 의 요청만 리소스에 대한 접근을 허용하려는 경우 다음을 전송한다.

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

이렇게 하면 https://foo.example 이외의 도메인은 cross-site 방식으로 리소스에 접근할 수 없다. 리소스에 대한 접근을 허용하려면, Access-Control-Allow-Origin 헤더에 요청의 Origin 헤더에서 전송된 값이 포함되어야 한다.

⭐️ Preflighted requests(프리플라이트 요청) ⭐️

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

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이고, 사용자 정의 헤더가 설정되었기 때문에 이 요청은 preflighted 처리된다.

(참고: 아래 설명 된 것처럼 실제 POST 요청에는 Access-Control-Request-* 헤더가 포함되지 않는다. OPTIONS 요청에만 필요하다.)

클라이언트와 서버간의 full exchange를 살펴보자. 첫 번째 통신은 preflight request/response이다.

// request 

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: http://foo.example
Access-Control-Request-Method: POST // preflight request의 일부, 실제 요청을 전송할 때 POST 메서드로 전송된다는 것을 알려줌
Access-Control-Request-Headers: X-PINGOTHER, Content-Type // 실제 요청을 전송할 때 X-PINGOTHER, Content-Type 헤더와 함께 전송된다는 것을 서버에 알려줌. 

// response

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 // 또다른 preflight request를 보내지 않고, preflight request에 대한 응답을 캐시할 수 있는 시간
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
### Requests with credentials

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>

// response

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]

Requests with credentials (인증정보를 포함한 요청)

다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법이다. request에 opiton값을 주는 것인데, fetch를 할 때 옵션에 credentials: 'include'를 주면 다음 두 가지 조건을 추가함으로써 정책 위반 여부를 확인하는 규칙이 엄격해진다.

  • Access-Control-Allow-Origin에는 *를 사용할 수 없으며, 명시적인 URL이어야한다.
  • 응답 헤더에는 반드시 Allow-Control-Allow-Credentials: true가 존재해야한다.

쿠키를 설정하는 리소스와 통신할 때,
브라우저는 Access-Control-Allow-Credentials: true 헤더가 없는 응답을 거부한다.

// request

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

// response 

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]

추가 정리

  • CORS란, 브라우저가 2개 이상의 서버와 연결되는 것을 예외적으로 허용하는 정책

  • '공인된 서버'에서 CORS를 허용하면, 서버 2개까지 연결 가능하다.
    (CORS 허용은 서버에서 하는 것이다!!)

  • preflight request에서 options 메소드의 역할

    • 서버에 CORS 허용하는지 물어본 후 ➡️ 허용한다는 응답이 오면 그때 본 요청을 보냄
  • preflight request는 simple request보다 보안이 좋다 (악성코드를 사전에 체크 가능)

    • 그럼에도 simple request를 사용하는 이유는? ➡️ 빠르다
    • 실제 개발할 때: 먼저 preflight 요청 보냄 ➡️ 안전하다고 판단되면, 이후 simple request로 통신
  • 같은 서버라고 판단하는 3가지 기준: 프로토콜,도메인,포트번호가 같으면 같은 서버 (외우자)

  • 클라이언트는 서버가 어떤 origin의 요청을 허용하는지 알 수 없다. Access-Control-Allow-Origin 의 value 정보는 서버만 알고 있고, 클라이언트는 origin이 허용되었는지 아닌지만 알 수 있다.

  • 모든 cross origin 요청이 preflight request를 발생시키는 것은 아니다.

HTTP 트랜잭션 해부 (공식 문서)

https://nodejs.org/ko/docs/guides/anatomy-of-an-http-transaction/

0개의 댓글