태그
요약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-Type
을CSS
로 지정해야만 다른 사이트에서 가져올 수 있음 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 내용을 요약.)
XMLHTTPRequest
나fetch
같은 모던 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>
는: - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
- Normal
script
elements pass minimal information to thewindow.onerror
for scripts which do not pass the standard CORS checks. To allow error logging for sites which use a separate domain for static media, use this attribute.
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 요청이라고 한다.
GET
,HEAD
또는POST
가 아닌 메서드로 요청할 때, 또는 안전한 목록의 일부가 아닌 헤더 또는 헤더 값을 포함하면 일반적이지 않은 요청으로 간주한다.- preflight 요청은
OPTIONS
메서드로 다음 두 헤더를 보낸다. 이 요청에는 크레덴셜이 절대 포함되지 않는다. Access-Control-Request-Method
: 실제 요청이 사용할 HTTP 메서드.Access-Control-Request-Headers
: 실제 요청이 사용할 헤더.- preflight 응답은 메인 요청을 해도 되는지 아닌지 여부를 나타낸다.
Access-Control-Max-Age
: preflight 응답을 몇초간 캐시해두는가 나타내고, 기본값은 5초다. 일부 브라우저에는 상한선이 있다. (Chrome은 600, Firefox는 86400)Access-Control-Allow-Methods
: "일반적이지 않은", 허용할 요청 메서드. 메인 요청이 크레덴셜 없이 전송된다면*
를 쓸 수 있다.CONNECT
,TRACE
또는TRACKE
는 보안상의 이유로 금지된 목록에 있으므로 허용할 수 없다.Access-Control-Allow-Headers
: "일반적이지 않은", 허용할 요청 헤더. 메인 요청이 크레덴셜 없이 전송된다면*
를 쓸 수 있다. 이렇게 하면 금지된 헤더 이름을 제외한 헤더를 허용하게 된다.- preflight 응답도 CORS 검사를 통과해야 하므로, 메인 요청이 크레덴셜을 포함하려면
Access-Control-Allow-Origin
및Access-Control-Allow-Credentials: true
헤더가 필요하며 상태 코드는 200-299 사이여야 한다. - 헷갈릴 수 있는데, 따라서 최종 요청에 대한 응답이 404이더라도 preflight는 200대여야 한다.
- 주의할 점은, preflight가 허용되더라도 실제 메인 요청에 대한 응답이 CORS 검사를 안 하는 건 아니다.
Access-Control-Request-Method: wibbley-wobbley
Access-Control-Request-Headers: fancy, here-we
Access-Control-Max-Age: 600
Access-Control-Allow-Methods: wibbley-wobbley
Access-Control-Allow-Headers: fancy, here-we