💻

[요약 정리] How to win at CORS

태그
요약Web
최종 편집
Dec 30, 2022 2:33 AM
발행일
November 12, 2021
💡
원문: https://jakearchibald.com/2021/cors/ 한글 번역이 있다는 걸 정리 다 하고 나서야 알았다.. 추가로 참고할 만한 글들 - MDN 공식 문서 https://developer.mozilla.org/ko/docs/Web/HTTP/CORS - CORS는 왜 이렇게 우리를 힘들게 하는걸까?

크롬의 유튜브 채널에서 HTTP 203이라는 시리즈를 진행하는 Jake Archibald가 쓴, CORS에 대해 자세히 이해할 수 있는 긴 글이다. 이해를 돕기 위해 CORS Playground라는 웹사이트도 같이 제공했다.

Cross-origin access without CORS

  • 30여년 전부터 브라우저는 다른 사이트의 이미지를 가져오도록 허용했다. 이 때 그 사이트의 퍼미션은 필요없었다.
  • 그리고 다른 사이트의 무언가를 가져오는 건 이미지로 멈추지 않았다. (script, link, iframe, video, audio, ...)
  • 1994년에 쿠키가 등장하면서 상황이 복잡해졌다. 쿠키에는 credentials 로 불리는 여러 인증 정보가 들어있었고, 다른 사이트의 리소스를 요청할 때에도 브라우저가 알아서 그 사이트의 쿠키를 보내기 때문에 여러가지 정보를 얻을 수 있다.
    • 예를 들어 로그인했을 때만 불러올 수 있는 이미지가 있다면, 이미지 요청에 성공했을 때만 load 이벤트가 오고 실패하면 error 이벤트가 올 것이므로 유저의 로그인 여부에 대해 알 수 있다.
    • CSS에 대해서는 좀 더 허점이 심했기 때문에 자주 공격에 이용되었다.
  • 이런 보안 허점들을 악용할 수 있었기 때문에 교차출처 리소스는 몇십년간 문제의 원흉이었다.

Locking things down

  • 위의 문제들을 (뒤늦게) 막기 위한 여러 발전이 있었다.
    • CSS 파일은 Content-TypeCSS 로 지정해야만 다른 사이트에서 가져올 수 있음
    • X-Content-Type-Options: nosniff header로, 옳은 Content-Type 없이는 이 CSS나 JS를 파싱할 수 없음을 명시
    • nosniff 가 다른 포맷들(HTML, JSON, XML)도 비슷하게 막도록 CORB(cross-origin read blocking)로 발전
    • 최근에는 SameSite쿠키를 이용해 다른 사이트로 쿠키를 안 보낼 수 있게 되었음
    • 파이어폭스와 사파리는 더 나아가서 사이트 하나하나를 완전히 격리시키는 시도를 하고 있음
  • 1995년에 Netspace에서 (JavaScript의 전신인) LiveScript와, HTML frame이라는 개념을 내놓았다.
    • 이는 보안 위험이 될 수 있었으므로, cross-frame scripting은 'origin이 같을 때'에만 실행할 수 있도록 했다.
    • same origin이 언제나 same owner를 뜻하진 않지만 아무튼 좋은 접근이었다.
    • 그리고 이 접근은 자바스크립트 + 프레임 관계뿐 아니라 모든 리소스 공유에서도 적용되었고, 여기에는 XMLHttpRequest 도 포함되었다. 즉 다른 사이트로 함부로 API 호출을 할 수 없게 된 것.
  • 어떤 웹 피처는 origin이 아니라 site 기반으로 작동한다.
    • 쿠키가 대표적인데, https://help.yourbank.com 와 https://profile.yourbank.com 는 다른 origin이지만 같은 site이다. yourbank.com 의 모든 서브도메인에 대해 (즉 사이트 레벨로) 쿠키가 공유되도록 설정할 수 있다.
    • 그런데 브라우저가 위 두 URL은 같은 사이트이면서 https://yourbank.co.uk 와 https://jakearchibald.co.uk 는 다른 사이트인 걸 어떻게 알 수 있는가?
    • Mozilla가 운영하는 public suffix list를 이용해서 알 수 있다.

Opening things up again

  • cross-origin 으로 img나 XMLHttpRequest 같은 강력한 API들을 사용할 수 있게 하려면 어떻게 해야 할까?
  • Credentials 빼고 요청을 보내는 전략
    • 브라우저 크레덴셜 외에 다른 방법들로 '안전함'을 보장하는 사이트들이 많아서 한계가 있다. 예를 들면 인트라넷.
  • 특정 사이트에게 cross-origin을 허용하는 전략
    • 대표적으로 Flash에서 이런 전략을 취했다. 사이트에 /crossdomain.xml 이 있는지 찾아보고 거기에 명시된 규칙대로 리소스를 공유하기.
    • 여기에도 몇 가지 문제가 있었다.
      • 전체 오리진의 동작을 바꿔버리기 때문에 특정 리소스에 대해서만 룰을 정할 수 없다.
      • 요청을 보낼 때 먼저 crossdomain.xml 부터 봐야 해서 요청이 2개씩 간다.
      • 팀이 커질수록 이 파일의 오너십이 애매해진다.
  • HTTP Header를 이용해 리소스별로 허용하는 전략
    • 결국 이걸로 통일. Access-Control-Allow-Origin 헤더.

Making a CORS request

(이 섹션은 이 글만으로는 완전히 이해를 못해서 MDN 문서를 보니 좀 더 이해가 됐다. 글 대신 MDN 내용을 요약.)

  • XMLHTTPRequestfetch 같은 모던 API들은 HTTP Header를 읽어서 CORS를 허용할 수 있지만, 예전부터 사용되던 (<audio>, <img>, <link>, <script>, <video> ) API들은 crossorigin 속성을 명시해야 CORS를 허용한다.
    • anonymous: CORS requests for this element will have the credentials flag set to 'same-origin'.
    • use-credentials: CORS requests for this element will have the credentials flag set to 'include'.
  • 이 속성을 명시하면 요청하는 쪽에서 더 디테일한 정보를 얻을 수 있다. 예를 들어 <script> 는:

CORS requests

  • 기본적으로 CORS 요청은 크레덴셜을 제외한 채 이루어진다. 즉 쿠키, 인증서, Authorization 헤더, Set-Cookie response 등은 모두 무시된다. 단, same-origin 이라면 크레덴셜을 포함시킨다.
  • 예전에는 Referrer 헤더를 썼지만 이게 쉽게 조작되거나 제거될 수 있어서, 대신 브라우저는 Origin 헤더로 request를 만든 페이지의 origin을 제공한다.

CORS responses

  • 다른 오리진이 response에 접근할 수 있게 하려면 response에는 이렇게 헤더를 포함해야 한다.
Access-Control-Allow-Origin: * (또는 실제 origin을 명시)
  • 이 헤더를 이용해 허용이 되면, response body에 다음의 헤더 값들도 포함된다.
    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma
  • Access-Control-Expose-Headers 를 이용해 더 많은 헤더를 노출할 수도 있다.
  • Set-Cookie 헤더는 cross-site 쿠키 노출을 막기 위해 절대 제공되지 않는다.

CORS and caching

  • 오랫동안 캐싱되는 리소스에 CORS 붙이기
    • 특정 어셋의 캐시 주기가 길다면, 아마 파일명을 바꿔서 유저가 새로운 컨텐츠를 보게 만들 것이다.
    • CORS에서도 마찬가지다. Access-Control-Allow-Origin: * 를 긴 캐시 주기를 가지는 리소스에 추가하려면, 기존 리소스 URL 대신 새 리소스 URL을 써야 유저가 (헤더가 없는) 캐시된 버전을 보지 않게 된다.
  • 특정 조건에서만 CORS 붙이기
    • 쿠키와 함께 요청이 들어왔을 때만 Access-Control-Allow-Origin: * 를 붙여서 응답할 수도 있다.
    • Vary: Cookie 헤더를 응답에다가 항상 넣어라. 이게 있으면, CORS가 아닌 (그래서 쿠키가 포함된) 요청의 응답으로 private data가 온 것이 캐싱되더라도, 쿠키 없이 같은 URL에 대해 CORS 요청을 했을 때 캐시된 버전이 응답되지 않는다.
    • 많은 클라우드 스토리지 벤더들이 Vary 헤더를 응답에 포함하지 않고 있다. 조심해서 사용할 것.

Is it safe to expose resources via CORS?

  • 리소스가 프라이빗 데이터를 절대 포함하지 않는다면, Access-Control-Allow-Origin: * 를 넣어도 안전하다. 당장 하라.
  • 쿠키에 따라 프라이빗 데이터 포함 여부가 달라진다면, Vary: Cookie 응답을 포함한다는 가정 하에 Access-Control-Allow-Origin: * 를 넣어도 안전하다.
  • 인트라넷처럼 특정 IP 주소에 대해서만 허용하는 식의 리소스가 있다면, CORS는 안전하지 않다.

Adding credentials

  • cross-origin CORS 요청이 기본적으로 크레덴셜을 포함하지 않지만, 포함하게 만들 수도 있다.
const response = await fetch(url, {
  credentials: 'include',
});
<img crossorigin="use-credentials" src="…" />
  • 단, 이 때는 응답이 반드시 다음 헤더들을 포함해야 한다.
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://jakearchibald.com
Vary: Cookie, Origin
  • 크레덴셜을 요청하려면 Access-Control-Allow-Origin 값이 * 가 될 수 없고, 무조건 요청한 origin과 동일한 값이어야 한다.
  • 이 응답이 어떤 식으로든 캐싱될 수 있다면, Vary 헤더의 존재가 아주 중요하다. 브라우저뿐 아니라 CDN에게도 마찬가지다. 이 헤더를 써서, 응답이 특정 request header의 값에 따라 달라지는 걸 명시해야만, 틀린 Access-Control-Allow-Origin 값으로 캐시된 응답이 가는 걸 방지할 수 있다.

Unusual requests and preflights

  • "일반적이지 않은" 요청을 할 때는 브라우저가 해당 출처에게 먼저 요청을 보내도 괜찮은지 물어보는데, 이를 preflight 요청이라고 한다.
    • GETHEAD 또는 POST가 아닌 메서드로 요청할 때, 또는 안전한 목록의 일부가 아닌 헤더 또는 헤더 값을 포함하면 일반적이지 않은 요청으로 간주한다.
  • preflight 요청은 OPTIONS 메서드로 다음 두 헤더를 보낸다. 이 요청에는 크레덴셜이 절대 포함되지 않는다.
    1. Access-Control-Request-Method: wibbley-wobbley
      Access-Control-Request-Headers: fancy, here-we
    2. Access-Control-Request-Method : 실제 요청이 사용할 HTTP 메서드.
    3. Access-Control-Request-Headers : 실제 요청이 사용할 헤더.
  • preflight 응답은 메인 요청을 해도 되는지 아닌지 여부를 나타낸다.
    1. Access-Control-Max-Age: 600
      Access-Control-Allow-Methods: wibbley-wobbley
      Access-Control-Allow-Headers: fancy, here-we
    2. Access-Control-Max-Age : preflight 응답을 몇초간 캐시해두는가 나타내고, 기본값은 5초다. 일부 브라우저에는 상한선이 있다. (Chrome은 600, Firefox는 86400)
    3. Access-Control-Allow-Methods : "일반적이지 않은", 허용할 요청 메서드. 메인 요청이 크레덴셜 없이 전송된다면 * 를 쓸 수 있다. CONNECTTRACE 또는 TRACKE는 보안상의 이유로 금지된 목록에 있으므로 허용할 수 없다.
    4. Access-Control-Allow-Headers : "일반적이지 않은", 허용할 요청 헤더. 메인 요청이 크레덴셜 없이 전송된다면 * 를 쓸 수 있다. 이렇게 하면 금지된 헤더 이름을 제외한 헤더를 허용하게 된다.
  • preflight 응답도 CORS 검사를 통과해야 하므로, 메인 요청이 크레덴셜을 포함하려면 Access-Control-Allow-Origin 및 Access-Control-Allow-Credentials: true 헤더가 필요하며 상태 코드는 200-299 사이여야 한다.
    • 헷갈릴 수 있는데, 따라서 최종 요청에 대한 응답이 404이더라도 preflight는 200대여야 한다.
  • 주의할 점은, preflight가 허용되더라도 실제 메인 요청에 대한 응답이 CORS 검사를 안 하는 건 아니다.