다음에 올 것을 만들어가는 기술에 대한 심층 기사.

Cursor 마켓플레이스 백도어: VS Code 포크의 신뢰 문제

Cursor 마켓플레이스의 Claude Code 확장 백도어를 통해 VS Code 포크가 Microsoft의 확장 신뢰 모델을 물려받지 못하는 이유와 지금 확인할 점을 정리합니다.

확장 타일 그리드가 보이는 노트북, 한 타일은 붉은 빛으로 갈라져 있고 그림자가 키보드 쪽으로 뻗어 있는 모습
확장 그리드는 타일 뒤에 정상 코드가 있든 페이로드가 있든 똑같아 보입니다.

화요일 아침 한 개발자가 Cursor를 열고 Claude Code 확장의 업데이트를 발견해 설치합니다. 겉보기에는 달라진 게 없습니다. 확장은 정상적으로 로드되고 AI 어시스턴트도 잘 동작합니다. 그런데 점심때쯤 그 머신의 ~/.ssh/authorized_keys에는 공격자의 Ed25519 키가 들어가 있고, 인터넷상의 서버로 역방향 SSH 터널이 신호를 보내고 있으며, Apple 시스템 프로세스로 위장한 LaunchAgent가 재부팅 후에도 이 모든 것을 살려 두고 있습니다. Phorion이 공개한 Cursor 마켓플레이스 백도어의 이야기입니다. 보안 업계에서 몇 년째 우려해 온 문제, 즉 VS Code 포크는 Microsoft의 코드베이스는 물려받아도 Microsoft의 신뢰 모델은 물려받지 못한다는 점을 가장 깔끔하게 보여준 사례라고 생각합니다.

개발자 PC에서 인시던트 대응을 오래 해온 입장에서 이번 사건이 특히 씁쓸한 이유는 배포 방식이 너무 평범하기 때문입니다. 피싱도 없고 제로데이도 없습니다. 그냥 변조된 확장 번들, 버전 2.1.312와 2.1.313이 마켓플레이스에 올라갔을 뿐이고, 개발자들은 그 마켓플레이스가 이미 신뢰하는 것과 똑같이 생겼다는 이유로 기본적으로 신뢰했습니다. Anthropic의 공식 채널은 깨끗했습니다. 누군가 Cursor의 퍼블리싱 파이프라인에 접근해서, Anthropic이 해당 레지스트리용으로 빌드한 적 없는 확장의 수정본을 밀어 넣은 것입니다. 피해 범위는 11시간 동안 자동 업데이트를 받은 모든 Cursor 사용자였습니다.

실제로 백도어가 한 일

이 임플란트는 압축된 extension.js 번들 앞에 즉시 실행 함수 표현식(IIFE)을 덧붙인 형태였습니다. 주입된 앞부분 이후의 코드는 정상 확장과 똑같이 보였습니다. 같은 activate, deactivate export, 같은 UI 코드, 같은 문자열이었죠. 여기가 중요합니다. "업스트림과 번들을 diff해서 확인한다"는 식으로 탐지하고 있다면 이 부분에서 뚫립니다. 악성 부분은 수 MB짜리 정상 파일 위에 얹힌 수백 바이트의 추가 자바스크립트에 불과하기 때문입니다. Node가 모듈을 로드하는 순간 실행되며, 확장 자체의 activate 콜백보다 먼저 돌아갑니다. 사용자가 무언가를 클릭하거나 Claude를 호출하거나 확장과 상호작용할 필요가 전혀 없습니다.

페이로드는 4단계로 구성되어 있고, 솔직히 말해 꽤 능숙하게 짜여 있습니다:

  • 키 심기. 공격자가 준비한 Ed25519 공개키를 svc-telemetry라는 주석과 함께 ~/.ssh/authorized_keys에 추가하고, 권한은 0700/0600으로 맞춥니다.
  • SSH 활성화. sshd가 실행 중이도록 시도합니다. Linux에서는 systemctl, macOS에서는 원격 로그인, Windows에서는 PowerShell로 OpenSSH 서버를 설치하는 방식입니다.
  • 역방향 터널. -R *:0:localhost:22 옵션으로 공격자가 운영하는 서버에 아웃바운드 SSH 연결을 엽니다. 공격자는 이 터널을 거슬러 피해자의 22번 포트에 접근해 심어 둔 키로 로그인할 수 있습니다. 별도의 C2 프로토콜은 필요 없습니다. SSH 자체가 C2입니다.
  • 지속성과 정찰. macOS에서는 com.apple.sysmond.plist라는 이름의 사용자 LaunchAgent, Linux에서는 @reboot cron 항목, Windows에서는 시작 프로그램 폴더를 백업 경로로 씁니다. 그리고 호스트명, 사용자명, git 이메일, 공인 IP, 그리고 /KEY|TOKEN|SECRET|PASS|AWS|GCP/i 패턴에 맞는 환경변수 이름을 최대 40개까지 담아 일회성 비콘을 보냅니다.

마지막 항목은 이해할 가치가 있을 만큼 영리합니다. 비콘은 AWS 시크릿 키를 빼내지 않습니다. 대신 여러분이 그 키를 가지고 있다는 사실을 빼냅니다. 일종의 타깃팅 목록입니다. 공격자는 환경에 살아 있는 클라우드 자격 증명을 가진 클라우드 엔지니어가 어느 감염 머신에 있는지 한눈에 파악하고, 터널을 통해 그 머신부터 되돌아갑니다.

우연히 사람들을 살린 macOS 버그

인시던트 responder로서 어둡게 웃기다고 느낀 부분이 여기입니다. 이 악성코드의 macOS 활성화 경로는 고전적인 스크립팅 실수 때문에 망가져 있습니다. 코드는 권한 없는 사용자 권한으로 systemsetup -getremotelogin을 실행하고, 출력이 /Off/i와 일치할 때만 원격 로그인을 켜려고 시도합니다. 그런데 macOS에서 이 명령은 관리자 권한 없이는 실행을 거부합니다. 표준 출력으로 "You need administrator access to run this tool... exiting!"을 찍고, 종료 코드는 0입니다.

// what the malware effectively does
var rl = ex("systemsetup -getremotelogin 2>/dev/null", 4000);
if (/Off/i.test(rl)) {
det("sudo -n systemsetup -setremotelogin on >/dev/null 2>&1");
}

에러 문자열에는 "Off"가 없으므로 정규식이 실패하고, 따라서 활성화 단계는 실행되지 않습니다. 스크립트는 "SSH가 꺼져 있는지 확인할 권한이 없다"를 "SSH는 이미 켜져 있다"로 해석해 버린 셈입니다. 실패한 명령이 0을 반환하는 것은 셸 스크립팅에서 가장 오래된 함정 중 하나인데, 이번에는 시스템 관리자가 아니라 악성코드 작성자를 물었습니다. 원격 로그인이 이미 켜져 있지 않은 Mac에서는 역방향 터널은 정상적으로 연결되지만, 터널을 통해 들어오는 접속은 닫힌 22번 포트에 부딪힙니다. 키와 지속성 요소는 여전히 설치되므로 머신이 감염된 상태는 그대로지만, 대화형 접근 경로는 처음부터 죽어 있는 셈입니다.

이걸로 안심하면 안 됩니다. 사용자가 원래 원격 로그인을 켜 두었거나 나중에 켜기만 하면, 심어진 키는 authorized_keys에서 기다리고 있습니다. Windows 경로에도 비슷한 버그가 있습니다. PowerShell 명령을 하드코딩된 /bin/sh를 거치는 헬퍼로 파이프합니다. Linux의 cron 로직은 반복 실행 시 사용자의 기존 crontab을 날려 버릴 수도 있습니다. 사람이 짠 악성코드에는 사람의 버그가 있습니다. 하지만 다음 달에 나올 것에는 그런 버그가 없을 겁니다.

포크 마켓플레이스가 Microsoft의 신뢰 모델을 물려받지 못하는 이유

이 부분이 구조적인 핵심이고, 엔지니어링 리드라면 여기서 잠시 멈춰 생각해 보셨으면 합니다. 공식 Visual Studio Marketplace에서 확장을 설치하면 그 뒤에는 실제 파이프라인이 있습니다. 도메인 검증을 거친 퍼블리셔 계정, 게시 시점 스캔, 삭제 절차, 그리고 법무팀이 있는 회사가 있죠. 완벽하지는 않습니다. 악성 확장이 빠져나간 적도 있습니다. 하지만 이것은 시스템입니다.

Cursor 같은 VS Code 포크는 라이선스 문제 때문에 그 공식 마켓플레이스를 쓸 수 없어서 Open VSX나 자체 인프라 같은 대체 레지스트리를 바라봅니다. 에디터 UI는 바이트 단위로 익숙합니다. 확장 페이지에는 같은 이름, 같은 아이콘, 그럴듯한 퍼블리셔 이름이 보입니다. 하지만 원래 마켓플레이스를 신뢰할 수 있게 만든 통제 장치는 모두 재구현되었거나 아예 없습니다. 퍼블리셔는 누가 검증했나요? 번들은 누가 스캔했나요? 아티팩트가 주장하는 소스 저장소에서 빌드되었는지 확인할 수 있나요? 이번 사건에서 변조된 버전은 Microsoft 레지스트리에는 아예 존재한 적이 없고 포크의 레지스트리에만 있었던 것으로 보입니다. 신뢰는 순전히 겉모습에 있었던 겁니다.

패키지 레지스트리 세계에서도 같은 영화를 본 적이 있습니다. LiteLLM PyPI 침해가 성공한 이유도 코드가 아니라 퍼블리싱 권한이라는 약한 고리 때문이었습니다. 확장은 한 가지 점에서 패키지보다 더 나쁩니다. 락파일 문화도, npm ci 같은 재현성도, 해시 고정도 없습니다. 에디터는 조용히 자동 업데이트를 하고, 한 Hacker News 댓글 작성자가 발견했듯이 VS Code의 자동 업데이트 지연은 "신뢰할 수 있는 퍼블리셔"에는 적용되지 않습니다. 그 목록은 여러분이 설정할 수도 없습니다. 즉, 여러분의 확장 세트는 사용자 권한 전체로 실행되며, 여러분이 자는 동안 스스로 업데이트되는 고정되지 않은 실시간 의존성 트리입니다.

똑같이 생긴 두 개의 상점 전면, 하나는 견고하고 다른 하나는 뒤에서 받쳐진 속 빈 가짜 외관으로 드러난 모습
포크된 마켓플레이스는 앞에서 보면 똑같아 보이지만, 그 뒤의 신뢰 인프라는 겉치레일 수 있습니다.

AI 코딩 도구는 직접 고른 권한 상승

마켓플레이스 소동을 걷어내고 실제 표적이 무엇이었는지 보죠. AI 코딩 확장은 색상 테마가 아닙니다. IDE의 권한, 즉 여러분의 권한으로 실행되고, 전체 업무가 소스 트리를 읽고, API 자격 증명을 쥐고, 점점 더 많이 여러분을 대신해 셸 명령을 실행하는 것입니다. 에이전틱 코딩 CLI가 권한을 어떻게 다루는지에 대해 글을 쓸 때 반복해서 드러난 주제는, 유용한 에이전트와 위험한 에이전트를 가르는 것은 능력이 아니라 정책이라는 점이었습니다. child_process를 실행할 수 있는 확장은 나쁜 배포 한 번이면 무엇이든 실행하게 됩니다.

한 댓글 작성자는 이렇게 직설적으로 말했습니다. 이런 도구를 쓰는 사람들은 사실상 매일 curl-pipe-bash를 실행해 온 셈이고, 우리는 10년 동안 주니어 개발자에게 그러지 말라고 가르쳐 왔다고요. 과장이긴 하지만 아주 조금만 과장입니다. curl | bash와 자동 업데이트되는 에이전틱 확장의 차이는 후자가 UI가 더 예쁘다는 것뿐입니다. 저는 전에도 CLI 도구가 기본값으로 백그라운드 데몬을 설치하면 안 된다고 주장한 적이 있습니다. 이번 사건은 그 데몬을 다른 누군가가 설치하고 여러분은 전혀 동의한 적 없는 상황이 벌어졌을 때 무슨 일이 생기는지 보여줍니다. IDE가 종료된 뒤에도 살아남는 분리된 ssh 프로세스는 제가 리뷰에서 늘 지적하는 바로 그 행동 패턴인데, 공격자가 신뢰받는 업데이트 채널을 통해 그걸 배포한 것입니다.

지금 당장 플릿을 점검하는 방법

조직 안에서 Cursor를 쓰는 사람이 있다면 이를 뉴스가 아니라 진행 중인 인시던트로 다루세요. 포렌식 분석 보고서에 나온 신호가 강한 지표는 다음과 같습니다:

# macOS / Linux: the planted key is the cleanest signal
grep -s svc-telemetry ~/.ssh/authorized_keys && echo "IMPACTED" || echo "key not found"
# Check persistence artifacts
ls ~/Library/LaunchAgents/com.apple.sysmond.plist 2>/dev/null   # macOS
ls ~/.config/.smt ~/.config/.smtk 2>/dev/null                   # macOS + Linux
crontab -l 2>/dev/null | grep -F .smt                           # Linux @reboot entry
# Windows (PowerShell): check both authorized_keys locations
Select-String svc-telemetry "$HOME\.ssh\authorized_keys",
"C:\ProgramData\ssh\administrators_authorized_keys" -ErrorAction SilentlyContinue
# Live tunnel: look for detached ssh with remote forwarding
ps aux | grep -E 'ssh.*-R' | grep -v grep

경험에서 나온 몇 가지 헌팅 팁입니다. 첫째, 주석만 보지 말고 키 지문을 확인하세요. 임플란트는 svc-telemetry 주석 문자열로 중복을 판단하고, 정리 작업도 같은 문자열을 기준으로 할 수 있습니다. 둘째, 터널용 키 파일은 임시 디렉터리에 .stk_로 시작하고 뒤에 호스트명과 홈 경로의 MD5에서 파생된 10자리 16진수가 붙은 이름으로 존재합니다. 머신마다 결정적이므로 정확한 파일명을 계산해서 검색할 수 있습니다. 셋째, 표준 사용자의 macOS 활성화 시도는 버그 때문에 실제로 실행되지 않으므로 EDR 프로세스 텔레메트리에 나타나지 않습니다. 로그에 systemsetup -setremotelogin이 없다고 해서 감염이 없다는 뜻은 아닙니다. 키와 LaunchAgent가 진짜 증거입니다.

키를 찾았다면 해당 머신은 전면적인 인시던트 대응 절차를 거쳐야 합니다. 비콘에 노출되었을 수 있는 환경변수 이름의 자격 증명은 전부 교체하세요. 대부분이 해당될 겁니다. 정규식이 AWS, GCP, 이름에 KEY나 TOKEN이 들어간 모든 것을 잡아내기 때문입니다. 머신의 접근 권한을 폐기하고 이미지를 다시 설치하세요. 키만 지우고 끝내면 안 됩니다. 공격자는 대화형 셸 접근을 했거나 시도했을 수 있고, 임플란트 코드만으로는 무엇을 건드렸는지 전부 열거할 수 없습니다. 여기서 클라우드 인증 로깅의 사각지대가 문제가 됩니다. 확장 업데이트 시점부터 공개 시점까지의 기간 동안, 영향받은 사용자의 자격 증명 기준으로 클라우드 감사 로그에서 콘솔이나 API 사용 내역을 확인하세요.

열쇠 꾸러미에서 붉게 빛나는 키 하나를 손전등 빛이 비추고, 케이블이 어둠 속으로 이어져 있는 모습
심어진 키는 라벨이 아니라 지문으로 찾으세요. 라벨은 정리 작업에서 가장 먼저 지워지는 부분입니다.

하드닝: 확장만이 아니라 에디터 자체를 격리하라

사고가 끝나면 모두가 무엇을 바꿔야 하냐고 묻습니다. 제 입장은 분명합니다. 감사만으로는 안전한 확장 생태계에 도달할 수 없으니, 격리하는 쪽으로 가야 합니다. 일반적인 트레이드오프는 AI 에이전트의 격리 대 모니터링에서 다뤘는데, 이번 사건이 그 격리 사례입니다. 이번에는 모니터링이 아무것도 알려주지 못했습니다. 외부 보안 업체가 우연히 번들을 분해해 보기 전까지는요.

  • 확장 버전을 고정하고, 넓은 권한을 가진 확장은 자동 업데이트를 끄세요. 포크에도 같은 원칙을 적용하세요. 업데이트 피로가 생기겠지만 인시던트 대응보다는 훨씬 싸게 먹힙니다.
  • 개발자 워크스테이션의 이그레스 필터링. 여기서 공격 모델 전체는 VPS IP로 나가는 아웃바운드 SSH 연결에 의존했습니다. 개발 머신이 임의의 아웃바운드 22번 포트를 열 수 없다면 터널 단계는 그 자리에서 죽습니다. 이 방식을 쓰는 곳은 거의 없지만, 적용했다면 이 임플란트를 완전히 무력화했을 것입니다.
  • SSH 상태를 카나리아로 감시하세요. 원격 로그인이 켜지는 경우, authorized_keys에 새 키가 추가되는 경우, 그리고 sshd를 돌릴 이유가 없는 머신에서 sshd 프로세스가 뜨는 경우에 경고를 보내세요.
  • 서버를 인벤토리 관리하듯 AI 도구도 인벤토리로 관리하세요. 어떤 확장과 에이전트 CLI가 승인되었는지, 어떤 권한을 가지는지, 어떤 마켓플레이스에서 왔는지 알아야 합니다. "우리 Cursor 설치가 어느 레지스트리에서 오는가"에 어깨만 으쓱한다면, 그게 다음 분기 프로젝트입니다.
  • 가능하면 샌드박스나 컨테이너에서 도는 에이전트를 쓰고, 자격 증명은 환경에 상시 두지 말고 세션별로 주입하세요. 비콘의 환경변수 이름 수집이 통하는 이유는 우리가 프로세스 환경에 비밀을 그냥 방치해 두기 때문입니다.

자체 검증 파이프라인이 없는 확장 마켓플레이스는 마켓플레이스가 아니라 설치 버튼이 달린 파일 호스트일 뿐입니다.

불편한 결론

Anthropic은 침해당하지 않았습니다. 침해된 것은 Cursor의 마켓플레이스였고, 개발자에게는 어느 쪽 빌드를 누가 배포했든 확장이 곧 제품이기 때문에 Anthropic의 브랜드가 피해를 떠안았습니다. 이것이 포크 생태계의 경제학입니다. 가치가 큰 표적을 공격하는 가장 싼 방법은 그 표적의 사용자가 신뢰하는 가장 약한 레지스트리를 치는 것입니다. AI 코딩 어시스턴트는 그 교차점에 완벽하게 놓여 있습니다. 설치 기반은 거대하고, 로컬 권한은 최대이며, 자격 증명은 어디에나 있고, 공급망은 보안 수준이 제각각인 여러 레지스트리에 아티팩트를 포크하고 재배포합니다.

이번 사건은 악성 버전이 약 11시간 만에 내려갔고, macOS 버그가 피해를 제한했습니다. 대응 자체는 괜찮았습니다. 문제는 구조이고, 그 구조는 바뀌지 않았습니다. 여러분의 에디터는 설치한 모든 것을 여러분의 권한으로 실행하고, 조용히 업데이트하며, 점점 더 많은 환경에서 보안팀 누구도 평가해 본 적 없는 레지스트리에서 패키지를 받아옵니다. 이그레스 규칙을 고치고, 확장을 고정하고, 에이전트를 인벤토리로 관리하세요. 다음번 퍼블리셔 침해에서는 우리 사용자를 구해 줄 정규식 버그가 없을 겁니다.