무슨 일이 있었나
연구자 분석을 정리한 SafeState의 글에 따르면, 공격은 대략 다음과 같이 진행됐습니다:
- Polymarket 사이트는 DebugBear의 CDN에서 실사용자 모니터링(RUM) 스크립트를 불러오고 있었습니다. 지극히 평범한 구성입니다. 수천 개의 사이트가 수십 개의 업체와 같은 방식을 씁니다.
- 공격자는 그 파일을 악성 JavaScript로 바꿔치기했습니다. 브라우저가 매번 업체에서 새로 가져오기 때문에, 모든 방문자가 즉시 새 버전을 받았습니다.
- 스크립트는 활성화된 Polymarket 세션이 있는지 확인하고, 브라우저 저장소에서 계정 데이터, 지갑 정보, 거래 API 자격 증명을 읽은 다음, Polymarket 자체의 서명 흐름을 이용해 포지션을 옮기거나 보유 자산을 팔아 공격자 지갑으로 보냈습니다.
곱씹어 볼 만한 대목이 있습니다. 연구자들에 따르면 하드웨어 지갑도, 강력한 비밀번호도 소용이 없었습니다. 스크립트에는 개인 키가 전혀 필요하지 않았기 때문입니다. 사용자 자신의 브라우저 세션 안에서, 로그인한 사용자로서 행동했습니다.
같은 보고서에 따르면 Polymarket의 조치는 해당 벤더 스크립트를 제거하는 프로덕션 커밋이었습니다. 거래는 같은 날 오후에 재개됐습니다. SafeState는 올해 이 플랫폼을 덮친 벤더 스크립트 사고가 이번이 처음이 아니라는 점도 지적합니다.
내 앱에 왜 중요한가
cdn.some-vendor.com을 가리키는 script 태그는 여러분의 코드와 똑같은 권한으로 실행됩니다. DOM을 읽고, localStorage와 sessionStorage를 읽고, 사용자의 자격 증명으로 API를 호출하고, 페이지에 보이는 내용을 바꿀 수 있습니다. ‘내 코드’와 ‘출시 주간에 붙여 넣은 분석 스니펫’ 사이에는 샌드박스가 없습니다.
빠르게 출시한 앱이나 AI로 만든 앱에서는 특히 중요합니다. 이유는 다음과 같습니다:
- 스크립트가 쌓입니다. ‘분석 추가해 줘’, ‘채팅 버블 추가해 줘’, ‘에러 추적 추가해 줘’ 같은 프롬프트마다 태그가 하나씩 늘어나기 쉽습니다. 나중에 지우는 사람은 거의 없습니다.
- 토큰이 브라우저 저장소에 있습니다. 많은 스타터 템플릿이 세션이나 API 토큰을 localStorage에 보관하는데, 그곳은 페이지의 어떤 스크립트든 읽을 수 있습니다.
- 대개 Content-Security-Policy가 없습니다. CSP가 없으면 주입되거나 바꿔치기된 스크립트가 어디서든 코드를 더 불러오고 어디로든 데이터를 보낼 수 있습니다.
업체의 빌드 파이프라인을 감사할 수는 없습니다. 하지만 악성 스크립트가 끼칠 수 있는 피해는 제한할 수 있습니다.
내 앱을 점검하는 방법
1. 페이지가 불러오는 모든 스크립트를 목록으로 만드세요. 앱을 열고 DevTools의 Network 탭에서 JS로 필터링한 뒤 새로고침합니다. 또는 중요한 페이지마다 콘솔에서 다음을 실행하세요:
[...document.scripts].map(s => s.src || '(inline)').filter(Boolean)자사 도메인이 아닌 것은 모두 적어 두세요. 마케팅 사이트뿐 아니라 로그인 후 페이지에서도 확인해야 합니다. 태그 관리자는 HTML 소스에 나타나지 않는 스크립트를 추가로 불러올 수 있습니다.
2. 그중 integrity 속성이 있는 태그를 확인하세요. 콘솔에서:
[...document.querySelectorAll('script[src]')]
.filter(s => !s.src.startsWith(location.origin))
.map(s => ({ src: s.src, sri: !!s.integrity }))3. 응답 헤더를 확인하세요. curl -sI https://yourapp.com 을 실행하고 Content-Security-Policy를 찾습니다. 없거나, script-src * 또는 nonce 없는 'unsafe-inline'이 들어 있다면 제한 효과가 거의 없습니다.
4. 세션이 어디에 저장되는지 확인하세요. DevTools의 Application에서 Local Storage와 Session Storage를 살펴봅니다. 액세스 토큰, 리프레시 토큰, API 키가 보인다면 페이지의 어떤 스크립트든 읽을 수 있습니다. Polymarket 페이로드가 한 일이 바로 그것입니다.
해결 방법
필요 없는 것은 제거하세요. 가장 효과가 큰 단계입니다. SafeState가 운영자에게 권하는 사항은 서드파티 스크립트 목록을 관리하고 계정, 결제, 거래 페이지에서 꼭 필요하지 않은 태그를 걷어내는 것에서 시작합니다. 로그인, 결제, 설정 페이지에 히트맵 도구가 필요한 경우는 드뭅니다.
남기는 스크립트는 Subresource Integrity(SRI)로 고정하고 검증하세요. 업체가 버전이 붙은 파일을 제공한다면 특정 버전을 참조하고 해시를 추가합니다. 파일이 바뀌면 브라우저가 실행을 거부합니다:
<script
src="https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js"
integrity="sha384-REPLACE_WITH_REAL_HASH"
crossorigin="anonymous"></script>해시는 직접 검토한 바로 그 파일에서 직접 생성하세요:
curl -s https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js \
| openssl dgst -sha384 -binary | openssl base64 -A트레이드오프도 알아 두세요. SRI는 업체가 버전 없는 URL에서 그대로 갱신하는 스크립트를 깨뜨립니다. 모니터링과 분석 스니펫에서는 흔한 방식입니다. 이런 스크립트는 고정된 사본을 직접 호스팅하고 의도적으로 업데이트하거나, 업체의 실시간 파일을 신뢰한다는 점을 받아들이고 민감한 페이지에서는 빼 두세요.
Content-Security-Policy를 추가하세요. CSP는 어떤 출처가 스크립트를 제공할 수 있는지, 페이지가 어떤 출처로 데이터를 보낼 수 있는지 브라우저에 알려 줍니다. Next.js나 Express 앱이라면 다음과 같이 시작할 수 있습니다:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example-vendor.com;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none'여기서 CSP가 해 줄 수 있는 일은 현실적으로 봐야 합니다. 업체 자체 도메인이 허용되어 있다면, 그 도메인에서 제공되는 악성 파일은 CSP로 막을 수 없습니다. CSP가 막는 것은 그 스크립트가 새로운 도메인에서 코드를 끌어오거나, connect-src에 없는 임의의 엔드포인트로 훔친 데이터를 보내는 것입니다. 먼저 Content-Security-Policy-Report-Only로 배포해 무엇이 깨지는지 확인하세요. Content-Security-Policy 추가하기 가이드에서 단계별로 안내합니다.
세션 토큰을 JavaScript가 닿지 않는 곳으로 옮기세요. 세션 토큰을 HttpOnly, Secure, SameSite가 지정된 쿠키로 옮기면 페이지 스크립트가 읽을 수 없습니다. 페이지가 열려 있는 동안 악성 스크립트가 사용자로서 요청을 보내는 것까지 막지는 못하지만, 토큰을 빼내 다른 곳에서 재사용하는 것은 막을 수 있습니다. Secure, HttpOnly, SameSite 쿠키 플래그를 참고하세요.
프런트엔드 키를 깨끗하게 유지하세요. 스크립트가 번들을 읽을 수 있다면 그 안에 넣어 배포한 시크릿도 모두 읽을 수 있고, 봇들은 이미 유출된 키와 .env 파일을 찾아다니고 있습니다. 클라이언트 측 JavaScript에 노출된 API 키 가이드를 확인하고, 프로덕션 환경의 소스맵을 배포하고 있지 않은지 점검하세요. 이미 한 번 놀란 적이 있다면 다운타임 없이 앱 시크릿을 교체하는 방법을 다룬 글이 뒷정리에 도움이 됩니다.
핵심 체크리스트
DevTools를 닫기 전에 다음을 확인하세요:
- 공개 페이지와 로그인 후 페이지의 모든 서드파티 스크립트를 목록으로 만들었다.
- 더 이상 쓰지 않는 스크립트를, 특히 로그인·결제·계정 페이지에서 제거했다.
- 업체가 지원하는 경우 버전을 고정하고 integrity 해시를 추가했다.
- 고정할 수 없는 스크립트는 직접 호스팅하거나 민감한 페이지에서 뺐다.
- script-src와 connect-src를 엄격하게 설정한 Content-Security-Policy를 배포했다.
- 세션 토큰을 localStorage가 아닌 HttpOnly 쿠키에 보관한다.
- 누군가(또는 어떤 AI 에이전트)가 새 태그를 추가할 때마다 이 점검을 다시 실행한다.
내 앱의 현재 상태 확인하기
빠르게 기준선을 잡고 싶다면, AppSafe는 Content-Security-Policy 설정 여부를 포함해 보안 헤더를 점검하고, 소유권이 확인된 전체 스캔에서는 Subresource Integrity와 취약한 JavaScript 라이브러리 같은 프런트엔드 문제도 살펴봅니다. 리포트가 어떤 모습인지는 샘플 리포트에서 볼 수 있고, AI로 만든 앱을 위한 출시 전 체크리스트와 함께 활용해도 좋습니다.
Polymarket은 거래를 중단한 지 약 1시간 만에 원인을 찾아냈습니다. 많은 소규모 팀은 바뀐 스크립트를 알아차리기까지 훨씬 오래 걸릴 겁니다. 오늘 AppSafe 무료 스캔을 실행해서 내 앱의 현재 상태를 확인해 보세요.
