CORS(Cross Origin Resource Sharing)에 대해 모르거나 이게 어떻게 작동하는 건지 제대로 설명하는 글을 찾는 이들을 위한 글이다.

나는 원래 블로그의 어원에 맞게 내가 삽질한 내용을 정리하고 나중에 같은 일이 일어났을 때 참고하는 용도로 글을 적었지만 CORS의 경우엔 이 쉬운 개념을 제대로 이해하질 못해서 고생하는 어린 사람들이 많아 그 때마다 이 글의 링크를 주기 위해 글을 작성하게 되었다.

이 글에선 CORS가 왜 태어났고 어떤식으로 동작하는 지에 대한 설명을 할 뿐 서버에 어떻게 적용하는 지에 대해선 설명하지 않는다. 그에 대해선 다른 블로그 글들이 넘쳐나기 때문에 다른 곳을 참조하길 바라고 이 글은 CORS 관련 헤더들을 추가하라고 해서 추가는 했는데 이게 왜 이렇게 되는 건지 모르겠거나 그로 인해 필요 이상의 삽질을 하고 있는 느낌이 드는 사람에게 필요한 글이다.

대다수의 블로그 글에서 설명하는 CORS는 “보안을 위해 존재하는 헤더다“가 전부다. 하지만 보안이라고 하면 이게 없었을 당시에 뭐가 문제가 되었는 지 알아야 하고 그래야 제대로 보안을 할 수 있으면서 제대로 사용할 수도 있다. 그래서 난 CORS가 없던 시절에 어떤 보안 취약점이 있었는지, CORS가 생기면서 어떻게 막을 수 있게 되었는지, 그리고 CORS로는 막을 수 없는 보안 취약점에 대해 설명하려고 한다. 이 셋을 이해하게 된다면 CORS는 껌 포장을 뜯는 것처럼 쉽다는 걸 알 수 있게 될 것이다.

CORS가 없던 시절에 발생하던 보안 취약점

첫번째로 말하자면 CORS는 서버사이드의 보안이 아니다. 언제까지나 클라이언트측 보안이고 CORS를 적용한다고 해서 서버가 더 안전해 지는 건 절대 아니다. 그 점을 유의하며 읽자.

우리의 유저 말숙이를 예시로 들어 설명해 보자. 말숙이는 컴퓨터 전문가가 아니고 당신과 같은 일반 유저다. 웹서핑도 하고 페이스북도 쓰고 쇼핑도 한다. 이 말숙이가 우리의 웹사이트 HowBadWeAre.com에 접속했다고 한다.

말숙이는 컴퓨터 전문가나 프라이버시에 집착을 하는 유저가 아니므로 자바스크립트도 돌아가고 같은 브라우저에서 페이스북도 로그인이 되어 있는 상황이다. 여기서 우리의 HowBadWeAre.com에 담긴 스크립트는 Ajax 기술을 사용해 facebook.com의 메인 페이지를 요청하고 내용을 받아 온다. 그 내용에는 말숙이가 페이스북에 들어갔을 때와 마찬가지로 말숙이의 이름, 말숙이와 친구들이 쓴 게시물 등이 모두 들어있다. 우리는 그 정보를 이용해서 우리의 사이트에 “말숙아 안녕?”이라는 메시지를 띄웠다.

말숙이는 당황스러울 것이다. 처음 들어온 웹사이트에서 내 이름과 나에 대한 정보를 알고 있으니 말이다. 물론 이에 신경 쓰지 않는 보안불감증 유저들도 있을 것이다. 그들을 위해 페이스북 글을 더 뒤져서 생일과 주소를 알아낸 다음 생일에 깜짝선물을 보내 보자.

CORS 이후의 웹

말숙이는 CORS를 제대로 지원하는 브라우저를 사용해 다시 HowBadWeAre.com에 접속했다. 이번에도 우리의 웹사이트는 facebook.com에 요청을 보내고 내용을 읽어오길 시도하지만 우리의 브라우저가 그걸 허용하지 않는다. 페이스북이 정보를 아예 주지 않는 것은 아니다. 다만 브라우저와 다음과 같은 이야기를 한다.

Facebook: 브라우저야, 니가 요청한 정보에 대한 응답을 주긴 주는데 이 정보는 우리 웹사이트에서(Same Origin) 요청한 게 아닌 이상 Javascript 코드에 결과값을 넘기지 말아줘

그렇게 해서 브라우저는 HowBadWeAre.com에서 실행 된 코드에 결과값을 넘기질 않게 된 것이다.
Request blocked

하지만 우리는 여기에서 짚고 넘어가야 할 것이 있는데 콘솔에는 분명 Cross-Origin Request Blocked 라고 뜨지만 실제로는 요청이 정상적으로 전달 되고 응답까지 왔다. 브라우저가 우리에게 결과값을 보여주지 않을 뿐.

실제로 패킷을 캡처해서 보자.

Request blocked to 192.168.1.1
Request actually send. Wireshark capture

CORS로 막을 수 없는 보안 문제

Same Origin Policy 정책과 CORS를 통해 우리는 기본적으로 서버측에서 허용하지 않는 한 자바스크립트 코드에서 타 사이트에 요청한 결과를 얻을 수 없는 것을 알 수 있었다. 하지만 실제로는 요청이 제대로 전달이 되었고 브라우저가 그 정보를 우리의 코드에게 주지 않는 것일 뿐인 것도 알았다. 그럼 자연스레 이런 공격 방법이 떠올라야 정상이다.

결과는 못 얻어도 요청은 제대로 간다고? 그럼 요청을 보내서 사용자를 즐겁게 해보자

현실에서 아래와 같은 예제들이 작동한다면 굉장히 곤란하지만 일단 이런 예시를 들 수 있다. (현실에선 CSRF 토큰과 리퍼러 체크 등으로 간단하게 방어가 가능하다. 다만 리퍼러만 믿는 건 바보짓이다)

  • <img src="https://facebook.com/SignOut" />
  • <img src="http://192.168.0.1/admin.php?cmd=ChangePw&pw=randompw" />
  • <img src="https://auction.com/buyProduct/?id=someExpensiveProduct" />

Cross-Origin-Allow-From: *는 대체 무엇인가

이 글을 쓰려고 하던 당시에 후배가 마침 CORS에 대한 질문을 했다.

그래서 도대체 Cross-Origin-Allow-From: *를 해버리면 CORS가 없던 시절이랑 뭐가 달라요?

결론부터 말하자면 없던 시절이랑 똑같다. 다만 다른 점은 없던 시절엔 저게 기본이었고 지금은 명시적으로 저렇게 허용을 해 줘야 브라우저가 클라이언트측 코드에게 결과값을 넘겨 준다는 것이다. 주로 API 서버 등과 같이 어디서든 요청과 결과를 필요로 하는 페이지에 저런 정책을 적용한다. 서버 전체에 적용하면 아까 든 예시처럼 우리 말숙이의 생일과 주소를 알아내서 생일선물을 보낼 수도 있다는 걸 명심하자.

요약

  • CORS 관련 헤더들은 클라이언트(브라우저)측 보안이다. 브라우저가 요청 한 리소스는 제대로 요청과 결과가 온다. 자바스크립트 코드쪽에서 확인을 할 수 없도록 브라우저가 막을 뿐.
    따라서 본인의 사이트에서 요청한 게 아니라고 해서 정보가 전달 되지 않는 것이 아니다. curl등으로 요청 할 수도 있고 패킷을 캡처해도(TLS도 디코드 해야겠지만) 그 정보는 훤히 보인다 (그걸 또 리퍼러로 체크하는 호구들은 인류의 미래를 위해 웹 개발자 일을 접어라).
  • 요청은 제대로 전달이 되기 때문에 CSRF(Cross Site Request Forgery) 공격은 가능하다. 국내 공유기의 경우 DNS 서버를 바꿔버리는 파밍 공격이 되지만 어느 제조사인지는 굳이 말하지 않겠다.
  • Cross-Origin-Allow-From: *을 통해 모두 허용을 해버리면 과거의 Same Origin Policy가 없던 시절과 정확히 똑같이 작동한다. 한 마디로 방화벽 내리기.