Claude Code 데스크톱 미리보기 취약점과 점검 포인트
10월 14일 Noma Labs는 SneakPeek를 공개했는데, 이는 Claude Code Desktop의 미리보기 창에서 악용 가능한 취약점 체인입니다. 공개 저장소의 악성 HTML 파일은 Claude가 이를 미리 볼 때 JavaScript를 실행할 수 있었고, 홈 디렉터리의 파일을 읽어 공격자의 서버로 전송할 수 있었습니다. Noma가 개념 증명(proof of concept)이 읽을 수 있었다고 밝힌 목록에는 SSH 개인 키, AWS 키, npm 및 GitHub 토큰, Kubernetes 및 Docker 자격 증명이 포함됩니다. Anthropic은 이를 CVSS 8.7(High)로 평가했습니다.
이 문제는 수정되었습니다. Noma의 타임라인에 따르면 Anthropic은 8월 17일에 Claude Desktop 1.30096.1 이후 버전에 자동 업데이트를 통해 수정 사항을 배포했습니다. 앱이 스스로 업데이트된다면, 이미 두 달 전부터 이 수정 사항을 가지고 있었던 셈입니다.
저는 에이전트가 작성한 코드를 읽기 위한 IDE인 Reado를 만들고 있어서, "사용자에게 파일을 보여주는" 부분에 존재하는 버그는 저에게도 정면으로 해당되는 문제입니다. 이 글은 왜 이 취약점이 이미 닫혔음에도 이해할 가치가 있는지, 그리고 해당 내용을 읽은 뒤 제 앱에서 무엇을 확인했는지에 대한 이야기입니다.
무슨 일이 있었나
Claude가 HTML 파일을 읽거나 편집할 때 Claude Code Desktop은 이를 미리보기 창에 표시하므로, 결과를 바로 볼 수 있습니다. Noma는 그 창 주변에 세 가지 방어가 있었다고 설명합니다.
script-src 'none'가 포함된 콘텐츠 보안 정책(Content Security Policy)으로, JavaScript가 실행되지 않도록 함;- 페이지를
data:URL로 로드해 불투명한 출처(opaque origin)를 부여함으로써 로컬 파일을 가져올 수 없게 함; - 렌더러가 보내는 요청을 가로채는 dev-server 프록시의 요청 필터.
문제는 각 방어가 적용되는 위치에 틈이 있었다는 점입니다. 256KB 미만의 파일은 CSP가 적용된 data: URL로 로드되었습니다. 256KB를 초과하는 파일은 임시 파일로 작성된 뒤 file:// URL로 로드되었는데, 그 경로에는 CSP가 적용되지 않았습니다. Chromium에서 file:// 페이지는 다른 로컬 파일을 fetch()할 수 있습니다. 그리고 요청 필터는 실행 중인 dev 서버를 프록시할 때만 활성화되었고, 디스크의 정적 파일을 렌더링할 때는 활성화되지 않았습니다.
따라서 공격 방법은 다음과 같습니다. HTML 파일을 256KB 이상으로 부풀린 뒤 그 안에 스크립트를 넣고, 누군가가 그것을 열어 보기를 기다리는 것입니다. Noma는 이를 테스트 커버리지 리포트처럼 위장했는데, 이렇게 큰 파일이 있어도 이상하지 않은 파일이기 때문입니다. 피해자는 저장소를 클론한 뒤 Claude에게 커버리지 리포트를 보라고 요청합니다. Claude가 파일을 읽고, 창이 이를 렌더링하며, 스크립트가 실행됩니다.
이 과정에서 어떤 시점에도 권한 요청은 없었고, 저는 바로 그 점이 계속 마음에 걸립니다. Claude는 명령을 실행하거나 파일을 쓰지 않았습니다. 그저 파일을 읽었을 뿐인데, 이는 에이전트가 할 수 있는 가장 무해한 행동처럼 보입니다. 그런데 코드가 애플리케이션의 렌더러 안에서, 결과를 보여주는 과정의 부수 효과로 실행된 것입니다.
첫 번째 수정과 그 우회 방법
Noma는 7월 4일에 이 버그를 보고했습니다. Anthropic의 첫 번째 패치는 페이지에서 file://로 향하는 fetch()와 XMLHttpRequest 호출을 차단했습니다. Noma는 같은 fetch()를 blob: URL에서 생성한 Web Worker 안으로 옮겼는데, 이 컨텍스트는 필터가 막지 않았고, 그 결과 파일이 다시 유출되었습니다. 그들은 이 우회 방법을 7월 24일에 보고했으며, 이는 패치가 재검토를 위해 준비된 바로 그날이었습니다. 8월 3일 Anthropic은 첫 번째 수정이 불완전함을 확인하고, Noma의 표현에 따르면 "CSP, file:// 로딩, 그리고 Worker 우회"를 포괄하는 완전한 수정에 착수했습니다. 그 수정은 8월 17일에 배포되었습니다.
Noma의 보고서는 Anthropic이 "완전한 계층형 수정에 도달할 때까지 모든 우회 방법에 반복적으로 대응했다"고 평가하고 있으며, 타임라인도 이를 뒷받침합니다. 하지만 우회 자체가 교훈입니다. 첫 번째 패치는 개념 증명에서 사용한 API만 막았습니다. 공격자는 다른 API를 사용했습니다. 완전한 수정은 대신 조건 자체를 겨냥했습니다. CSP, file:// 로딩, 그리고 Worker였습니다.
이 사건에서 내가 얻은 세 가지
두 번째 코드 경로는 두 번째 보안 모델이다. 제 추측으로는 256KB 기준은 성능 결정이었을 것입니다. 큰 data: URL은 느리기 때문입니다. 누구도 "큰 파일에는 CSP를 적용하지 말자"고 결정하지는 않습니다. 하지만 임계값 하나가 조용히 큰 파일들을 다른 규칙이 적용되는 경로로 보냈습니다. 어떤 기능에 같은 일을 수행하는 방법이 두 가지라면, 각각에 대한 보안 검토가 필요합니다. 더 나은 방법은 아예 하나의 방식만 두는 것입니다.
권한 시스템은 앱이 아니라 그 안의 에이전트를 지킵니다. 코딩 에이전트를 안전하게 만들기 위해 우리가 하는 일의 대부분은 에이전트에 관한 것입니다. 이 명령은 승인하고, 프로젝트 밖의 쓰기는 거부하고, 셸은 샌드박스로 가둡니다. 이 모든 것은 여기에도 적용되어 있었지만, 어느 것도 관련되지 않았습니다. Carlos는 버그에 대한 dev.to 글에 남긴 댓글에서 이를 잘 표현했습니다. “여기서는 저장소의 파일이 공격자였고, 뷰어가 도구였다.” 저장소 안의 내용을 렌더링하는 것이라면 무엇이든(미리 보기, Markdown, 노트북, 이미지, 내장 브라우저) 에이전트가 관여하든 말든 공격 표면의 일부입니다.
“이 파일을 검토하세요”는 그 파일을 가장 덜 신뢰하는 순간입니다. SneakPeek를 유발한 요청은 검토였습니다. 이 커버리지 보고서를 보라는 것이었죠. 그 순간은 그 파일을 신뢰하는 사람이 아무도 보기 전에 보는 때이므로, 그것을 보여 주는 표면은 앱에서 가장 강력한 것이 아니라 가장 제약이 많은 것이어야 합니다.
Reado가 저장소를 보여 주는 방식
Reado도 같은 문제를 해결해야 합니다. Reado는 저장소 안의 파일을 보여 주기 위해 존재하며, 그 대부분은 사용자가 직접 작성하지 않은 것들입니다. Noma의 글을 읽은 뒤, 저는 SneakPeek의 세 가지 질문을 기준으로 Reado가 그런 파일을 보여 주는 모든 지점을 살펴봤습니다. 파일의 코드가 실행되는가, 페이지가 디스크를 읽을 수 있는가, 무언가를 밖으로 보낼 수 있는가.
.html파일은 소스 코드로 열립니다. Reado는 Markdown, Jupyter 노트북, 이미지와 PDF를 렌더링합니다. 그 외의 모든 것, HTML을 포함해, 에디터 안의 텍스트로 취급됩니다. 커버리지 보고서는 실행되는 것이 아니라 읽는 것입니다.- Markdown과 노트북 출력 내부의 HTML은 sanitizer를 거칩니다. README에는 HTML(가운데 정렬 로고, 배지,
<details>)이 가득하므로 Reado는 이를 rehype-sanitize를 통해 렌더링하며, 이 과정에서 스크립트, 이벤트 핸들러,javascript:URL을 제거합니다. - sanitizer 뒤에서는 앱의 Content Security Policy가 적용됩니다. Reado와 함께 제공되는 스크립트만 실행됩니다(
script-src 'self'). 이 규칙은 워커에도 적용되므로blob:Worker 트릭도 시작점이 없습니다. 프레임도 없고, 페이지는 앱 자신 말고는 어디에도 요청(fetch(),sendBeacon())을 보낼 수 없습니다. - 파일 크기가 로딩 방식을 바꾸지 않습니다. 500KB짜리 README도 5KB짜리 파일과 같은 파이프라인을 거칩니다. 일정 크기를 넘으면 Reado는 대신 멈춥니다. 큰 파일을 열기 전에 묻고, 너무 큰 파일은 아예 열지 않습니다. 절대 다른 로더로 전환하지 않습니다.
- 로컬 이미지는 열려 있는 프로젝트에서만 가져옵니다. 디스크의 이미지에 Reado가 사용하는 프로토콜은 빈 범위에서 시작해, 파일을 열면 프로젝트 폴더로만 확장되며, 디스크의 나머지 부분으로는 절대 넓어지지 않습니다. SVG는
<img>태그를 통해 표시되며, 이 경우 SVG 스크립트는 실행되지 않습니다. PDF는 캔버스에 그려집니다.
같은 점검 과정에서 제가 “파일을 보여 준다”고 생각하지 않았던 표면에서 두 가지 버그도 드러났습니다. Reado의 내장 브라우저, 즉 개발 서버를 미리 보는 패널입니다. 이 패널은 저장소가 제어할 수 있는 두 곳에서 URL을 가져옵니다. 설계 코멘트는 다른 모든 Reado 코멘트와 마찬가지로 .reado/에 저장되며, 고정된 페이지의 URL을 담고 있습니다. 그리고 에이전트는 같은 폴더의 명령 파일을 통해 이 패널을 조작합니다. 그래서 복제된 저장소가 file: 또는 data: 페이지를 가리키는 코멘트나, 패널이 열리기를 기다리는 명령을 미리 담아 보낼 수 있었습니다. 둘 다 수정되었습니다. 이제 패널은 http와 https 페이지만 열며, 모든 호출자가 거치는 Rust 계층의 한 곳에서 검증됩니다. 또한 패널이 열릴 때 이미 존재하던 명령은 기록만 되고 실행되지는 않습니다. 수정 사항은 main에 반영되어 있으며 다음 릴리스에 포함됩니다.
공통된 핵심은 제가 다른 이유로 선택한 한 가지였습니다. Reado는 코드를 읽는 데 초점을 맞춰 만들어졌기 때문에, 파일의 기본 동작은 텍스트를 보여 주는 것입니다. 이는 실시간 미리 보기만큼 인상적이지는 않지만, SneakPeek 이후에는 오히려 다행이라고 생각합니다.
이러한 도구를 사용한다면, 또는 만든다면
Claude Code Desktop을 사용한다면 Claude Desktop 1.30096.1 이상인지 확인하세요. Noma의 글에는 GitHub advisory와 CVE가 아직 pending 상태라고도 나와 있으니, 이것들도 주시하세요.
저장소의 파일을 보여 주는 도구를 만들고 있다면, 이 버그에서 몇 가지 원칙을 얻을 수 있습니다:
- 복제한 저장소의 모든 파일은, 지루한 파일까지 포함해, 낯선 사람이 준 입력으로 취급하세요.
- 파일이 아무리 크더라도, 동일한 제한을 적용하는 하나의 렌더링 경로를 두세요.
- 제한은 API가 아니라 콘텐츠가 어디에서 오는지에 적용하세요.
fetch()를 막으면 공격자는 Worker를 노립니다. - 기본적으로는 소스를 보여주세요. 정말 필요한 것만 렌더링하세요.
Anthropic는 이 문제를 수정했고, 우회 방법도 막힐 때까지 계속 대응했습니다. 이 패턴은 Anthropic에만 해당하는 것이 아닙니다. 파일을 보여주는 모든 코딩 도구에는 이런 미리보기 창이 있습니다. 다른 사람이 먼저 보기 전에, 여러분도 자신의 도구를 점검해볼 가치가 있습니다.
출처
- 원문: https://dev.to/urion/sneakpeek-the-claude-code-bug-that-lived-in-the-preview-not-the-agent-15bm (2026-10-18 · Matteo Poli)