개요/배경
- CORS는 Cross-Origin Resource Sharing의 약자이다.
- Javascript 활용이 활발해지면서 동일 출처 정책(SOP)의 제한을 넘어서 사이트 간에 데이터를 교환(크로스 도메인)하려는 요구가 강해졌다.
- Javascript는 동일 출처 정책에 제한받기 때문에 크로스 사이트간 데이터교환(Ajax요청 등)이 불가능하다. (정확히는 요청은 가능해도 응답에 접근못한다.)
- 이 제한을 극복하고자 제안된 것이 CORS 이다.
- 조건이 갖추어진 환경(헤더에 접근 허용 도메인 지정 등) 하에서 데이터 교환을 할 수 있다.
- 중요한 것은 리소스를 가진 서버측에서 권한을 설정 해준다는 점이다.
- 클라이언트 측 요청은 여러 구현 방법이 있지만 주로 XMLHttpRequest를 사용한다.
XMLHttpRequest의 코드는 다음과 같다.
var req = new XMLHttpRequest();
req.open('GET', 'http://a-url');
req.onreadystatechange = function() {
if (req.readyState == 4 && req.status == 200) {
alert(req.responseText);
}
};
req.send(null);
- [CORS는 JSONP 의 대체수단으로 쓰일 수 있다.][1]
- [크로스 도메인 에러가 발생하면 브라우저에서 다음과 같은 메세지를 출력한다.][2]
XMLHttpRequest cannot load [FQDN].
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin '[FQDN]' is therefore not allowed access.
- CORS는 결국 SOP의 제한을 풀어주는 역할을 하므로 XSS공격을 포함한 잠재적인 공격 위험도 늘어나게 된다.
- 따라서 CORS를 사용할 때에는 설정에 문제가 없는지(취약점이 없는지) 잘 체크해야 한다.
CORS 역사
- 2004년에 Matt Oshry, Brad Porter, Michael Bodell 라는 사람에 의해 VoiceXML이라는 기술의 일부분으로써 크로스오리진 요청 스펙이 제안되었다.
- 후에 2006년에 보다 일반적인 기술로써 W3C에 초안이 제안되었고, 2009년에 이 초안의 이름이 Cross-Origin Resource Sharing로 바뀐다.
- 2014년 1월에 W3C의 추천기술로 채택되었다.
- 비교적 역사가 짧기 때문에 오래된 브라우저는 CORS에 대응하지 않을 수 있다.
CORS 요청 헤더
- 클라이언트(브라우저)측에서 CORS요청을 보낼 때 HTTP요청에 추가하는 헤더이다.
Access-Control-Request-Method
,Access-Control-Request-Headers
,Origin
세 헤더가 사용된다.
CORS 응답 헤더
다음 헤더들은 서버측(응답측)에서 지정하는 HTTP헤더이다. 다음은 CORS헤더를 포함한 HTTP응답 예이다.
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Allow-Headers: Content-Type, x-requested-with
Access-Control-Max-Age: 86400
Access-Control-Allow-Origin
서버 입장에서 자신의 사이트의 컨텐츠에 접근을 허용할 클라이언트 도메인을 지정할 수 있다. *
로 지정할 경우 모든 도메인으로 부터의 접근을 허용한다. 보안상 위험하다.
Access-Control-Allow-Methods
클라이언트측에서 사용가능한 HTTP 요청 메서드를 지정한다. POST, GET 등이다.
Access-Control-Max-Age
권한 확인을 위한 Preflight 요청이 캐시될 시간(초) 지정한다.
Access-Control-Allow-Headers
허용할 클라이언트 측 HTTP 요청 헤더 값을 지정한다. 예를들어 x-requested-with
라고 지정할 경우 Ajax 요청만 허용한다.
Access-Control-Allow-Credentials
가능한 값은 true이다. 필요없으면 이 헤더 자체를 생략하면 된다. 클라이언트측의 인증정보(세션 쿠키등)를 포함한 요청에 대해 응답을 허용할지를 지정한다. 이 헤더는 클라이언트측의 다음 코드와 쌍으로 동작한다. 자바스크립트의 req.withCredentials = true;
가 있어서 CORS요청시 인증정보가 함께 서버로 전송된다. 그리고 서버측에서 Access-Control-Allow-Credentials: true
헤더로 허용을 해주어야 클라이언트측에서 서버의 리소스에 접근할 수 있다.
var req = new XMLHttpRequest();
req.open('GET', 'https://xxx.com');
req.withCredentials = true;
CORS 동작과정 예
어떤 서버(서버A)가 다른 서버(서버B)에 리소스를 요청할 때를 생각해본다.
서버A는 다음과 같은 요청을 보낸다. 서버A의 도메인은 foo.example이다. Origin
헤더에 이 값이 지정되어 있다.
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
리소스를 가지고 있는 서버B는 HTTP응답에 다음과 같이 CORS 헤더를 추가해야 한다. 클라이언트측의 브라우저는 이를 보고 허용되었구나라고 판단하여 리소스에 접근시켜준다.
Access-Control-Allow-Origin: https://foo.example
프리플라이트(Preflight) 요청
- CORS 가 동작할 때 사실은 프리플라이트 요청이라는 것이 먼저 날라간다.
- 날기전에(Preflight) 날아도 되는지 확인하는 과정이라고 이해하면 될 것 같다.
- 날아도 된다는 확인이 되었을 때 실제 크로스 오리진 요청이 보내진다.
- 프리플라이트 요청은 HTTP의 OPTIONS 메서드를 사용해서 보내진다.
Access-Control-Request-Method
,Access-Control-Request-Headers
,Origin
세 헤더가 사용된다. 각각 어떤 HTTP 메서드를 사용하고 싶은지, 어떤 요청헤더를 사용하고 싶은지 어떤 오리진에서 보내는지를 나타낸다.- [다음과 같다.][4]
OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org
서버가 CORS에 대응하고 있다면 응답을 해주어야 한다.
HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
프리플라이트 요청이 필요한(존재하는) 이유
- 여기.)에 잘 설명되어 있다.
- 간단히 요약하면 서버가 CORS에 대응하고 있는지를 모르는 상태에서 요청을 보내고 (예를들어 DELETE요청과 같은 위험한 요청을 보내고), 그 요청이 서버에서 처리되어 버리면 위험하기 때문에 미리 서버가 CORS에 대응하고 있는지 체크한다는 흐름인 것 같다. CORS에 대응하고 있지 않다면 브라우저는 이후의 요청 (실제 요청)을 보내지 않는다. (마치 CSRF공격을 막는 메커니즘같다.)
- 이런 배경때문에 프리플라이트 요청은 CSRF대책으로도 활용된다.
프리플라이트요청이 CSRF 대책으로 활용되는 것에 대한 정보는 이하의 링크에서 확인할 수 있다.
- https://stackoverflow.com/questions/41148282/why-doesnt-pre-flight-cors-block-csrf-attacks
- https://portswigger.net/daily-swig/chrome-to-bolster-csrf-protections-with-cors-preflight-checks-on-private-network-requests : 크롬에서도 내부 네트워크 서버에 대한 CSRF대책으로 프리플라이트 요청을 활용하는 것 같다.
- https://www.apollographql.com/docs/router/configuration/csrf/ : Node.js의 GraphQL 서버인 apollo에서도 프리플라이트 요청을 활용해 CSRF 대책으로 사용한다.
프리플라이트 요청이 필요없는 경우
- 간단한 요청(simple request)라고 불리는 요청인 경우에는 프리플라이트 요청이 발생하지 않는다.
- 원래부터 존재했던 HTML폼도 크로스 오리진 요청이 가능하기 때문에, HTML 폼을 사용해서 전송되는 정도(간단한 요청)면 리스크가 크게 증가하지 않는다고 판단한 것 같다.
- 또한 간단한 요청이 아니더라고 동일한 오리진이면 프리플라이트 요청은 발생하지 않는다.
간단한 요청의 조건
간단한 요청의 조건은 다음 세 가지를 모두 만족하는 요청이다.
- 다음 중 하나의 메서드 (DELETE는 안된다.)
- GET
- HEAD
- POST
- 유저 에이전트가 자동으로 설정 한 헤더외에 수동으로 설정된 헤더는 다음의 헤더들만 가능
- Accept
- Accept-Language
- Content-Language
- Content-Type
- Content-Type 헤더는 다음의 값들만 가능 (
application/json
은 안 된다.)- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
참고
- https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
[1] https://en.wikipedia.org/wiki/Cross-origin_resource_sharing [2] http://ooz.co.kr/232 [3] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials [4] https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request