소스 보기, 키 복사, 전부 가져가기
한 보안 연구자가 ClickUp 페이지 소스를 열어 JavaScript에 하드코딩된 API 키를 발견했고, 그 키로 GET 요청 하나만 보내 이메일 주소 959개와 사내 기능 플래그 3,165개를 가져왔습니다. 그 키에는 권한 범위도, 속도 제한도, 만료일도 없었습니다.
한 보안 연구자가 clickup.com에 접속했습니다. 페이지 소스를 열었습니다. JavaScript에 하드코딩된 API 키를 찾았습니다. 복사했습니다. GET 요청 하나를 보냈습니다.
이메일 주소 959개와 사내 기능 플래그 3,165개가 돌아왔습니다. Home Depot, Fortinet, Autodesk, Tenable, Rakuten, Mayo Clinic의 직원들입니다.
문자열 하나. 요청 하나. 전부입니다.
어떻게 이런 일이 생기는가
누군가 프론트엔드에서 API를 호출해야 했습니다. API에는 인증이 필요했습니다. 그래서 키를 JavaScript에 넣었습니다. 배포하고, 넘어가고, 다음 스프린트로 갑니다.
이건 정교한 공격이 아닙니다. 익스플로잇도, 제로데이도, 사회공학도 없습니다. view-source:와 curl입니다. 브라우저 하나, 터미널 하나. 호기심 많은 인턴이 입사 첫날 하는 종류의 일입니다.
키에는 권한 범위가 없었습니다 — API가 노출하는 모든 것에 접근할 수 있었습니다. 속도 제한도 없었습니다 — 요청 하나가 전부를 반환했습니다. 만료일도 없었습니다 — 누군가 알아차릴 때까지 키는 계속 작동했습니다. 두 번째 요인도 없었습니다 — 문자열을 갖고 있다는 사실이 유일한 관문이었습니다.
돈의 관점
사람들은 데이터 노출에 대해 이야기합니다. 이 데이터가 얼마짜리인지 이야기해 봅시다.
포춘 500대 기업의 기업용 이메일 주소 959개. 위협 행위자들이 돈을 주고 사는 스피어피싱 대상 목록입니다. 이름, 직책, 그리고 이 기업들이 ClickUp을 쓴다는 사실 — 피싱을 작동하게 만드는 사회공학적 맥락입니다.
사내 기능 플래그 3,165개. 로드맵입니다. ClickUp이 무엇을 만들고, 무엇을 테스트하고, 무엇이 게이트 뒤에 있는지를 경쟁사에 알려 줍니다. 어떤 기능이 반쯤 만들어졌고 취약할 가능성이 높은지를 공격자에게 알려 줍니다.
이건 개인정보 유출 사고가 아닙니다. 사업 정보 유출입니다.
왜 계속 반복되는가
이번 달에 우리가 쓴 소스 내 자격증명 유출 사고는 이번이 네 번째입니다. Bitwarden의 CLI는 평문 파일이라서 자격증명이 수집됐습니다. Vercel의 환경 변수는 '민감함' 플래그가 기본값이 아니라서 복호화할 수 있었습니다. 한 개발자는 복호화 키가 같은 디스크에 있어서 Chrome 비밀번호 634개를 잃었습니다.
패턴은 언제나 같습니다. 자격증명이 문자열로 존재하고 — 파일 안에, 변수 안에, 페이지 소스 안에 — 무언가 그것을 읽습니다. 그 무언가는 달라집니다. 패턴은 달라지지 않습니다.
JavaScript 안의 API 키는 가장 심각한 버전입니다. 공격이 필요 없기 때문입니다. 키는 게시됩니다. 모든 방문자에게 전달됩니다. 브라우저가 내려받고, 렌더링하고, 마우스 오른쪽 버튼을 누르는 누구에게든 보여 줍니다.
달라졌어야 할 것
이 API 호출은 클라이어트 쪽의 정적 키로 인증했으면 안 됐습니다. 선택지는 이렇습니다.
- 백엔드 프록시. 프론트엔드는 키를 서버 쪽에 보관하는 자체 백엔드를 호출하고, 백엔드가 API 호출을 프록시합니다. 키가 브라우저에 도달하는 일은 없습니다.
- 세션 범위 토큰. 프론트엔드는 인증 후 짧은 수명의, 좁은 권한 범위를 가진 토큰을 받습니다. 만료됩니다. 인증된 사용자에게 허용된 일만 할 수 있습니다. 마스터 키가 아닙니다.
- 키를 아예 쓰지 않기. 데이터가 공개 데이터라면 인증 없이 제공합니다. 공개 데이터가 아니라면 인증되지 않은 JavaScript에는 제공하지 않습니다.
클라이언트 쪽 코드에 API 키를 하드코딩하는 것은 현관문 아래에 집 열쇠를 두고 주소를 공개하는 일입니다.
자격증명 수명주기 문제
이 ClickUp 키는 아마 한 번 만들어져 JavaScript 파일에 붙여넣고, 저장소에 커밋하고, 프로덕션에 배포한 뒤 다시는 생각하지 않았을 것입니다. 아무도 교체하지 않았습니다. 아무도 범위를 좁히지 않았습니다. 아무도 만료일을 정하지 않았습니다. 아무도 이 키가 무엇에 접근하는지 지켜보지 않았습니다.
대부분의 조직에 있는 대부분의 API 키의 수명주기가 이렇습니다. 급하게 만들어져 필요한 곳에 붙여넣고, 잊힙니다. 코드베이스, 설정 파일, CI/CD 파이프라인, 그리고 이제 보니 페이지 소스까지 쌓입니다. 각각은 잠기지 않는 문입니다.
질문은 여러분 조직에 이런 키가 있느냐가 아닙니다. 질문은 몇 개나 되느냐, 그리고 누군가 오늘 하나를 복사했는지 여러분이 알 수 있느냐입니다.