2P by GN⁺ | ★ favorite | 댓글 1개
  • Zoom의 로컬 웹서버 취약점은 많은 웹 개발자가 CORS 동작 방식을 잘못 이해할 때 보안 경계가 쉽게 무너질 수 있음을 보여줌
  • Zoom은 localhost:19421 로컬 서버와 통신하면서 AJAX 대신 이미지 크기로 상태 코드를 전달했고, 이는 CORS를 피하려는 우회 구현으로 해석됨
  • Chrome은 localhost 웹서버에도 CORS 헤더를 적용하며, 서로 다른 localhost 포트의 프론트엔드·백엔드 통신도 브라우저에서 지원됨
  • 더 안전한 설계는 로컬 서버가 REST API를 제공하고 Access-Control-Allow-Origin을 설정해 zoom.us의 JavaScript만 접근하게 제한하는 방식임
  • 같은 출처 정책을 우회하면 코드는 동작할 수 있지만, 로컬 서버의 권한 있는 기능이 인터넷의 모든 웹사이트에 노출될 수 있음

Zoom 로컬 웹서버가 만든 CORS 우회

  • 풀스택 컨설팅 현장에서 다양한 규모와 산업의 개발자들을 접하면서, 웹 개발자들이 CORS를 이해하지 못하는 문제가 반복적으로 보였음
  • 최근 Zoom 취약점에서 보안 연구자 Jonathan Leitschuh는 Zoom이 사용자 머신에 http://localhost:19421 웹서버를 띄운다는 점을 발견함
    • 사용자가 Zoom 링크를 열면 Zoom 웹사이트가 localhost 웹서버로 요청을 보내 네이티브 Zoom 앱을 실행함
    • 일반 AJAX 요청 대신 로컬 Zoom 웹서버에서 이미지를 로드하고, 이미지의 서로 다른 크기로 서버의 오류·상태 코드를 표현함
  • 브라우저가 localhost 서버의 CORS 정책을 무시한다는 해석은 틀렸고, Chrome은 localhost 웹서버의 CORS 헤더를 존중
    • Create React App 프론트엔드와 백엔드 API를 서로 다른 localhost 포트에서 실행할 때도 교차 출처 요청이 발생하며, 이는 모든 브라우저에서 지원됨
  • AJAX 요청이 차단되자 Zoom이 이미지 해킹으로 CORS를 우회한 것으로 보임
    • 그 결과 Zoom 웹사이트뿐 아니라 인터넷의 다른 웹사이트도 네이티브 클라이언트 동작을 트리거하고 응답에 접근할 수 있게 됨

안전한 대안과 남는 UX 문제

  • 안전한 구현은 localhost:19421의 웹서버가 REST API를 구현하고 Access-Control-Allow-Origin 헤더 값을 https://zoom.us로 설정하는 방식임
    • 이렇게 하면 zoom.us 도메인에서 실행되는 JavaScript만 localhost 웹서버와 통신할 수 있음
    • zoom.us는 백그라운드에서 Zoom 미팅이 자동으로 열리는 것을 막기 위해 iframe 렌더링을 차단하는 Content Security Policy 헤더도 둘 수 있음
  • 어떤 페이지든 브라우저를 zoom.us 미팅 링크로 리디렉션할 수 있는 문제는 여전히 남음
    • 다만 이는 소프트웨어 취약점보다는 Zoom이 선택한 사용자 경험에 가까움
    • 링크를 클릭했을 때 카메라와 마이크가 모르는 사람에게 갑자기 열리지 않을 것이라는 사용자 기대를 Zoom이 깨고 있음
    • 브라우저 기본 팝업을 UX 이유로 피하고 싶다면 앱 안에서 팝업을 보여줄 수도 있으며, Google Meet가 그런 방식을 잘 사용함
  • localhost에서 웹서버를 실행하는 것 자체가 위험한 시도이며, 특히 소프트웨어 설치 같은 권한 있는 기능을 인터넷의 모든 웹사이트에 제공해서는 안 됨
    • CORS는 이런 상황을 안전하게 처리하기 위한 장치이므로 우회하지 않아야 함

Zoom만의 실수가 아닌 CORS 혼란

  • Zoom이 실제로 CORS를 이해하지 못해서 이런 방식을 택했는지는 확실하지 않음
    • Reddit의 lerunicorn은 Firefox가 보안 출처에서 비보안 출처로의 XHR을 막을 수 있다고 봄
    • 하지만 Firefox는 origin이 localhost인 경우 이를 지원함
    • 네이티브 앱은 고유한 자체 서명 인증서를 생성할 수 있고, 브라우저 확장을 사용할 수도 있음
    • 어떤 경우에도 출처 필터링을 생략할 정당한 이유는 되지 않음
  • CORS 혼란은 Zoom만의 문제가 아님
    • Stack Overflow에는 Access-Control-Allow-Origin 관련 질문이 많이 존재
    • Express 예제 중에는 그대로 복사하면 애플리케이션이 취약해질 수 있는 불안전한 기본값을 권하는 페이지도 있음
    • 다른 벤더들도 Zoom과 동일한 취약점을 겪은 적이 있음
  • 개발자는 코드를 동작시키고 싶어 하지만, 같은 출처 정책을 통째로 우회하면 Zoom 사례처럼 로컬 권한이 외부 웹사이트에 노출됨
  • CORS 혼란은 숙련 개발자와 신규 개발자 모두에게서 보이며, CORS API가 지나치게 복잡한지 또는 CORS와 CSP 교육이 부족한지는 분명하지 않지만 현재 방식은 잘 작동하지 않고 있음

댓글과 토론

Hacker News 의견들
  • TFA도 CORS를 제대로 이해하지 못했거나 심하게 잘못 설명한 듯함
    Access-Control-Allow-Origin: https://zoom.us는 zoom.us 도메인의 JavaScript만 localhost 서버와 통신하게 보장하지 않음. 다른 웹사이트의 JavaScript도 localhost:19421로 요청은 똑같이 보낼 수 있음. CORS는 무언가를 제한하는 게 아니라 기본 제한을 완화하는 장치임. 이 헤더는 zoom.us에서 실행되는 JavaScript가 localhost:19421 응답을 읽을 수 있게 해줄 뿐이고, 요청 자체는 어쨌든 발생하므로 백엔드에서 부작용이 생기지 않게 해야 함

    • 이게 왜 최다 추천 댓글인지 모르겠음. OP가 맞고, 위 설명은 틀렸음
      GET 요청은 보내지지만 원래 멱등이어야 하므로 서버가 정상적으로 구현됐다면 부작용을 일으킬 수 없고, GET에서는 응답을 읽을 수 있느냐가 핵심임. 반대로 부작용을 가질 수 있는 비멱등 요청은 교차 출처 상황에서 실제 요청 대신 먼저 preflight OPTIONS 요청이 가며, OPTIONS 응답에 올바른 헤더가 없으면 실제 요청은 전송되지 않음
    • CORS가 그런 역할을 한다고 말할 수도 없다고 봄
      CORS에 대한 오해가 너무 널리 퍼져 있고 문서들도 종종 서로 모순돼서, 알 수 없는 상대가 제대로 구현했을 거라고 기대하기 어렵다. 어떤 프로토콜이 이 정도로 광범위한 혼란을 낳으면 한쪽이 올바르게 동작해도 다른 쪽이 그럴지 알 수 없음. 사람들이 다른 구현체에 맞춰 코드가 동작할 때까지 고쳤다면, 틀린 건 자기 쪽인지 상대 쪽인지도 흐려짐
    • 내가 이해한 바로는 preflight OPTIONS의 핵심 목적은 원래 허용되지 않을 HTTP 요청을 막는 것이고, 원래 허용되는 요청에는 CORS가 아무것도 하지 않음
      예를 들어 Content-Typetext/json인 POST는 OPTIONS preflight 없이 제3자 호스트로 보낼 수 없지만, multipart/form-data인 POST는 허용되며 CORS가 막지 않음. 그리고 엔드포인트가 Content-Type을 엄격히 확인하지 않고 JSON이라고 가정하면, 어떤 웹사이트든 사용자 조작 없이 POST를 보낼 수 있게 된 셈임
    • “안전한 메서드만 이야기한다고 가정”은 꽤 큰 가정임
      괜찮은 웹 개발자라면 GET/HEAD/OPTIONS가 상태를 바꾸게 만들면 안 되고, 회의 참가 같은 건 상태 변경임. PUT/DELETE도 멱등이어야 함. JSON 또는 폼이 아닌 형식의 POST API는 Content-Type 헤더를 확인해야 하며, PUT/PATCH/DELETE와 폼 형식이 아닌 Content-Type의 POST는 preflight를 유발해서 실제 요청이 서버에 도달하기 전에 CORS가 확인됨
    • 글의 “네이티브 앱은 고유한 자체 서명 인증서를 생성할 수 있다”는 부분도 문제가 있음
      인증서를 만들기만 해서는 동작하지 않고, 머신의 모든 브라우저 신뢰 저장소에 루트 CA 인증서로 설치돼야 함. 루트 CA의 개인키가 제대로 보호되지 않으면 어떤 웹사이트든 중간자 공격할 수 있으므로 최소한 이름 제한이 필요함(https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10). 그런데 Chrome은 2023년 v112 전까지 루트 CA에서 이것이 동작하지 않았고(https://alexsci.com/blog/name-non-constraint/), 그래서 중간 CA를 추가해 거기에 제한을 걸어야 했음. 물론 루트 CA 키는 버리는 게 맞음
      예전에 로컬 루트 CA를 쓰는 프로젝트에서 기본 제약을 추가한 적이 있는데, 루트 CA에 잘못 넣었고 모든 브라우저에서 테스트하지도 않았음
  • 더 많은 사람이 MDN의 CORS 문서를 읽었으면 좋겠음. CORS를 이해하려 할 때 큰 도움이 됐고, 여기 댓글을 보면 사람들이 이렇게까지 어려워하는 줄은 몰랐음
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS

    • 그 문서 하나면 단순한 출처 사례뿐 아니라 preflight 동작 방식까지 대부분 답이 됨
  • 이해하기 어려운 건 CORS만이 아니라, 많은 개발자가 위협 모델을 제대로 이해하지 못함
    설명을 들어도 왜 큰 문제인지 감이 잘 안 오는 경우가 많다. 특히 백엔드 개발자가 CORS를 설정하는 일이 많은데, CORS는 접근 권한 보호 장치가 아니라서 백엔드 입장에서는 별로 중요해 보이지 않음. 공격자는 못 가져간다고 느끼고, 프론트엔드 입장에서는 귀찮은 장애물처럼 보이기 쉽다. 이 글은 구체적인 예시를 잘 보여줌

    • 같은 개발자가 프론트엔드와 백엔드를 모두 작성한 프로젝트에서도 CORS 설정을 틀린 적이 있었음
      운영 담당자로서 로드 밸런서에서 다시 제대로 고쳤고, 적어도 애플리케이션은 이제 동작함. CORS는 이해하기 어렵지만, CORS가 막으려는 위협 모델뿐 아니라 웹 개발 전반, 특히 HTTP 프로토콜을 이해하지 못하는 개발자도 많다는 게 더 안타까움
    • CORS 위협 모델은 그리 어렵지 않음. 공격자가 사용자를 자기 사이트로 유도해, 당신의 사이트에서 어떤 행동을 하게 만드는 상황임
    • CORS는 꽤 이상한 기본 권한 모델 위에 쌓여 있어서 헷갈림. multipart/form-data는 괜찮지만 애플리케이션 JavaScript는 안 된다는 식임
    • 공격자와 방어자 관점에서 보면 위협 모델이 아주 자연스럽지는 않음
      CORS는 선택적이고, 다른 라이브러리나 도구는 그냥 무시할 수도 있음. CORS는 실제로 로그인한 인간 사용자에 대한 XSS와 CSRF를 막는 데만 의미가 있고, 그 외 공격 시나리오는 어차피 HTTP 헤더를 위조하는 스크립트나 프로그램을 쓰므로 무의미함. 그래서 사람들이 결국 모든 CORS 옵션을 켜버리는데, 이는 XSS와 CSRF를 허용하는 최악의 경우임
    • CORS는 사람들이 대역폭과 호스팅 자원을 쉽게 훔쳐 쓰지 못하게 하는 데는 훌륭함. 훔치려면 직접 프록시를 세워야 하고, 그러면 차단하기 쉬워짐
  • 이 댓글란은 정말 정보 수준이 낮아 보이고, 오히려 글쓴이의 요점을 그대로 증명함

    • 세대 차이일 수도 있음
      CORS가 생기기 전 웹 개발을 했다면 원래 교차 도메인 요청이 금지돼 있었고, CORS는 이 보안을 우회하기 위해 생겼다는 걸 이해함. 그래서 원하는 일을 하려면 CORS를 활성화하면 된다고 받아들이기 쉽다
      반대로 CORS 이후에 웹 개발을 배운 사람은 교차 출처 요청을 시도하고, 브라우저가 허용되지 않는다고 판단하며, CORS preflight를 시도하고, 실패하면 콘솔에 CORS 오류가 뜨는 흐름만 보게 됨. 내부 동작을 모르고 문서를 읽지 않은 채 추측하면 CORS가 요청을 막는 원인이라고 생각하고 “CORS를 비활성화”하려 하게 된다. 하지만 CORS는 문제의 원인이 아니라 해결책임
      같은 오해를 가진 사람들이 튜토리얼과 온라인 토론에서 자신 있게 반복하니 더 헷갈려짐
    • CORS가 직관적이지는 않지만, 문서를 읽으면 이해 가능함
      https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  • 댓글을 읽으며 나만 그런 게 아니라는 걸 확인했음. 아무도 CORS를 이해하지 못하는 이유는 너무 복잡하고 충돌이 많기 때문
    표준과 헤더도 계속 바뀌어서, 개발자들은 대개 동작할 때까지 이것저것 만지다가 제품을 배포하고 끝냄. 동작하더라도 개발자 콘솔에 오류와 경고가 남을 수 있지만, 겉으로 잘 돌아가면 그냥 건드리지 않게 됨

  • CORS를 이해하려면 먼저 동일 출처 정책을 이해해야 함
    특히 “왜 이게 필요한가?”가 어렵다면 여기서 시작하는 게 좋음: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy
    예전에 동일 출처 정책을 면접 질문으로 써봤지만, 다수의 지원자가 익숙하지 않아 그 질문으로는 얻을 수 있는 정보가 적었음

    • 프론트엔드 개발자를 뽑을 때는 꽤 좋은 질문이라고 봄
      웹 앱을 개발해왔다면 언젠가는 동일 출처 정책을 마주쳤어야 하기 때문임. 모른다면 보통 백엔드와 어떻게 통신했는지 등을 더 물어보게 됨. CORS 문제를 만났지만 가장 빠른 우회로만 적용하고 잊어버렸는지, 실제로 이해하려 했는지도 일부 역할에서는 유용한 신호가 됨
      백엔드 역할에는 덜 적합함. 모든 백엔드 개발자가 CORS 문제를 자주 겪는 프론트엔드 팀과 밀접하게 일한 건 아니기 때문임
  • CORS에 대해 기억나는 건 디버깅이 예상보다 훨씬 오래 걸리고, 브라우저 오류 메시지가 의도적으로 빈약하며, CORS 오류가 다른 실패 모드와 처음엔 구분하기 어렵다는 것임

    • CORS 오류는 “브라우저로 전송된 오류 메시지”가 아니라, 브라우저가 요청을 허용할 수 없다고 판단해 생성한 오류임
      물론 서버가 CORS 요청을 이해하지 못하고 이상한 응답을 반환하면, 그것이 결국 CORS 실패로 번역될 수는 있음
  • 댓글란을 보니 꽤 재미있어서 덧붙이면, 동일 출처 정책은 브라우저가 접근 권한 없는 웹사이트로 정보를 유출하지 않도록 보호하고, CORS는 그 보호를 약화할 수 있게 해줌
    예를 들어 동일 출처 정책은 example.comyoutube.com의 구독 목록을 가져오지 못하게 막음. 하지만 CORS를 쓰면 example.comyoutube.com/public/*에는 접근하도록 허용할 수 있음
    또 다른 용도로는 백엔드 API가 다른 프론트엔드 아래에서 동작해 데이터 탈취로 이어지는 걸 막는 효과도 있음. 예컨대 실제 서비스에는 로그인했지만 사용자는 g00gle.com에 있고, 모든 요청이 중간자 공격될 수 있는 상황을 막아줌

    • 정확히는 반대임. 이런 보안 문제를 막는 건 SOP이고, CORS는 SOP를 느슨하게 만들어 더 복잡한 애플리케이션 간 동작을 허용하는 기능임
  • 나도 그런 사람 중 하나임. CORS는 주기적으로 다시 공부해야 하는 주제이고, 늘 까먹어서 머리에 잘 안 남음
    백엔드 개발자라 CORS 문제를 거의 겪지 않아서 그런 듯함. 매일 쓰지 않는 건 잘 잊어버리는 편임

    • CORS와 CSP의 개발자 경험은 끔찍함. 브라우저들이 문제가 어디서 비롯됐는지 제대로 말해주지 않기 때문임
      정상적인 세상이라면 오류 메시지에 “응답 헤더”나 “meta 태그” 같은 힌트를 넣었을 텐데, 주요 브라우저 업체들이 수수께끼 같은 메시지를 쓰는 사람들을 고용한 것처럼 보임. Chrome의 “requested resource”가 그나마 낫지만 여전히 암호 같음
      더 나은 메시지는 예를 들어 https://bank.com 리소스가 CORS 헤더가 없어 교차 출처 요청을 허용하지 않는다거나, 현재 출처가 CORS 허용 목록에 없다는 식이어야 함. 네트워크 탭의 preflight 요청과 MDN 링크도 함께 보여줘야 함. CSP도 이 페이지의 CSP 헤더 때문에 리소스를 가져올 수 없고, 검사기의 페이지 요청 헤더나 meta 태그로 연결해주는 식이 낫다
    • CORS의 가장 큰 문제는 대부분의 오류가 프론트엔드 문제, 특히 브라우저 문제처럼 보이지만 실제 수정은 백엔드에서 해야 한다는 점임
    • 나도 비슷하게 느낌. 몇 번 CORS를 다뤄야 했던 상황은 “이 서버에서 뭔가를 가져와야 하는데 서버의 CORS나 CSP는 바꿀 수 없다”는 요구였고, 보안 용어로 말하면 “보안 시스템이 있는데 우회해야 한다”는 뜻임
      결국 대개 서버가 변조되지 않은 브라우저 요청으로만 접근된다는 가정에 의존함. Zoom 취약점은 클라이언트 쪽에서 CORS와 CSP를 우회하기 너무 쉬워서 생겼고, Zoom이 나쁘고 게으르고 어리석었던 건 맞지만 이런 모델을 계속 유지하는 커뮤니티도 책임이 있다고 느낌
  • 동일 출처 정책이 브라우저가 악성 스크립트를 실행해 정보를 유출하는 것을 어떻게 막는지는 이해함. Access-Control-Allow-Origin 헤더로 서버가 추가 출처를 신뢰한다고 선언해 SOP를 완화하는 것도 이해함
    그래도 Access-Control-Allow-Headers 헤더의 목적은 아직 모르겠음. 브라우저 보안을 개선하는 것 같지도 않고, 서버 보안은 더더욱 아님. 프로토콜 설계자가 “완성도를 위해” 넣은 건지 궁금함. 관련: https://stackoverflow.com/questions/17992042