본문으로 건너뛰기

© 2026 Molayo

YouTube요약2026. 06. 15. 09:57

단 하나의 PR이 NPM 레지스트리를 하이재킹했습니다...

요약

단 하나의 풀 리퀘스트(PR)를 통해 NPM 레지스트리의 100개 이상의 패키지가 침해된 공급망 공격 사례를 분석합니다. GitHub Actions의 'pull_request_target' 설정 오류를 악용하여 신뢰할 수 있는 게시 토큰을 탈취한 정교한 공격 방식을 다룹니다.

핵심 포인트

  • PR 생성만으로 CI 워크플로를 트리거하여 공격 가능
  • pull_request_target 옵션 사용 시 보안 취약점 발생
  • 신뢰할 수 있는 NPM 게시 기능을 우회한 공급망 공격
  • 악성 코드 확산을 방지하기 위한 데드 맨즈 스위치 위협

비디오: 단 하나의 PR이 NPM 레지스트리를 하이재킹했습니다...
채널: Fireship
길이: 6분 16초
출처: 자막 (자동 생성, 영어)

스크립트:
불과 며칠 전, 모든 오픈 소스 유지 관리자(maintainer)들의 최악의 악몽이 마침내 현실이 되었습니다. 아니, 이성들과 사교 활동을 강요받은 것은 아닙니다. 대신, 제가 수년 동안 주장해 온 것처럼 사실 꽤 긴 시간인 단 6분 만에, 주간 총 다운로드 수가 5,000만 회가 넘는 100개 이상의 패키지들이 공급망 공격 (supply chain attack)을 통해 침해되었습니다. 이 공격에서는 아무도 피싱을 당하지 않았고, 비밀번호가 유출되지도 않았으며, 토큰이 도난당하지도 않았습니다. 더욱 최악인 점은, 오염된 패키지들이 NPM의 신뢰할 수 있는 게시 (publishing) 기능을 통해 서명되고, 검증되었으며, 배포되었다는 것입니다. 이 기능은 주로 이러한 종류의 공격을 방지하기 위해 구축되었으며, 거의 2년 동안 권장되는 게시 설정이었습니다. 오늘 영상에서는 공격자가 React 생태계의 가장 큰 프로젝트 중 하나의 릴리스 파이프라인 (release pipeline)을 하이재킹할 수 있었던 영리한 트릭을 분석해 보겠습니다. 그 후 악성 코드가 어떻게 수백 개의 다른 패키지로 퍼져나갔는지 살펴보고, 당신이 시스템을 정리하려고 시도하는 순간 홈 폴더를 날려버릴 (nuke) '데드 맨즈 스위치 (dead man's switch)'에 대해 알아보겠습니다. 신이 우리 모두를 도우시길. 2026년 5월 14일이며, 여러분은 The Code Report를 시청하고 계십니다. 최근 삶이 유난히 흥미진진합니다. 왜냐하면 매일 아침 눈을 뜰 때마다, 쥐똥 크루즈 바이러스와 미니 샤이 훌루드 (mini shai hulud) 바이러스 중 어떤 것이 내 인생을 먼저 망가뜨릴지 궁금하기 때문입니다. 다행히 저는 똥 전문가는 아니니, 아무도 예상치 못한 상당히 정교한 공격을 통해 샤이 훌루드 프레리도그가 어떻게 다시 그의 작은 머리를 깨물었는지 분석하는 데 집중하겠습니다. 이 모든 것은 TanStack의 릴리스 프로세스 (release process)가 작동하는 방식에서 시작되었습니다. 새로운 풀 리퀘스트 (pull request)가 머지 (merge)될 때마다, GitHub Actions 워크플로 (workflow)가 시작되어 NPM 레지스트리에 새 버전을 게시하는 작업을 처리합니다. 하지만 이를 수행하려면, 지속적 통합 (continuous integration) 서버가 먼저 NPM으로부터 게시 토큰 (publish token)을 받아야 합니다.

요청이 정당하다는 것을 증명하기 위해, GitHub 자체가 어떤 워크플로 (workflow)가 어떤 리포지토리 (repo)의 어떤 브랜치 (branch)에서 실행 중인지를 명시하는 서명된 문구 (signed statement)를 생성합니다. 그러면 NPM은 해당 서명된 문구를 조직의 허용 목록 (allow list)과 대조하여 모든 조건이 일치할 때만 토큰을 전달합니다. 이 토큰은 CI의 캐시 (cache)에 몇 분 동안만 머물다가 빠르게 만료되므로, 전통적인 피싱 (fishing) 공격이 훔쳐갈 수 있는 것이 없습니다. 매우 완벽해 보이지만, 문제는 이 '미니 샤이 훌루드 (mini Shai Hulud)' 공격이 전통적인 방식이 아니었다는 점입니다. 작동 방식은 다음과 같습니다. 공격자는 TanStack 리포지토리를 포크 (fork)하고, 풀 리퀘스트 (pull request)를 생성한 뒤 즉시 닫았습니다. 비록 단순한 포크였고 아무도 그 풀 리퀘스트를 보지 못했음에도 불구하고, 단지 PR을 생성하는 것만으로도 게시 워크플로 (publishing workflow)를 실행시키기에 충분했습니다. 그리고 바로 이 지점에서 TanStack 팀의 실수가 발생했습니다. 워크플로의 트리거 (trigger)를 설정할 때 'pull request target' 옵션을 사용했기 때문입니다. 이는 해당 PR이 포크에서 생성되었더라도, 들어오는 모든 풀 리퀘스트가 메인 리포지토리의 권한과 함께 메인 리포지토리의 컨텍스트 (context) 내에서 실행됨을 의미했습니다. 그리고 그 권한은 공격자의 코드가 CI 서버의 공유 캐시 (shared cache)에 오염된 파일 (poisoned file)을 작성할 수 있을 만큼 충분했습니다. 이 공유 캐시는 GitHub Actions가 작업 (job) 간에 의존성 (dependencies)을 재사용하기 위해 사용합니다. 몇 시간 후, 관련 없는 다른 풀 리퀘스트가 메인 (main) 브랜치에 병합 (merged)되었고, 오염된 파일이 트리거되어 캐시에서 NPM 게시 토큰 (NPM publish token)을 탈취했습니다. 그리고 이를 이용해 84개의 완전히 새로운 TanStack 패키지들에 거대한 '샤이 훌루드 덤프 (Shai Hulud dump)'를 수행했습니다. 그 후, 만약 당신이 운 좋게(?) 해당 패키지 중 하나를 npm install 했다면, 악성 코드가 실행되어 당신의 시스템을 스캔하고 탐낼 만한 가치 있는 것들을 찾아냈을 것입니다. 만약 NPM 게시 토큰을 발견했다면, 공격자는 이를 사용하여 동일한 해킹 방식으로 새로운 오염된 버전들을 게시했습니다. 이것이 바로 TanStack의 문제가 모두의 문제로 번지게 된 과정입니다.

첫 번째 피해 그룹에는 Mistral AI, UiPath, Open Search, Guardrails AI, 그리고 Squawk의 관리자들이 포함되었습니다. 불과 몇 시간 만에 이 기업들 또한 오염된 패키지들을 NPM 레지스트리에 게시했습니다. 그리고 이 웜(worm)은 이들의 Python SDK를 통해 PyPI로 넘어가는 파격적인 행보를 보였습니다. 다음 날 아침까지 보안 기업 Akamai는 169개 패키지에 걸쳐 373개의 오염된 버전을 추적하고 있었으며, 웜은 더욱 영리해지고 있었습니다.

이 웜은 Claude Code GitHub 앱이 서명한 커밋을 위조하기 시작했습니다. 덕분에 악성 활동이 관리자들이 이미 익숙하게 보던 AI 생성 커밋들 사이에 자연스럽게 섞여 들어갔습니다. 또한 감염된 머신에서는 VS Code 내의 Claude Code에 직접 삽입되었기 때문에, 개발자가 악성 패키지를 삭제하더라도 에디터를 다시 여는 순간 웜이 스스로를 재실행했습니다. 그리고 가장 압권인 부분은, 만약 감염되었다면 이 멀웨어는 탈취한 GitHub 토큰이 여전히 유효한지 60초마다 조용히 확인하는 백그라운드 프로세스를 설치한다는 점입니다. 토큰이 만료되는 순간, '전쟁 범죄 모드(war crime mode)'를 활성화하여 루트 디렉토리를 초토화(nukes)해 버립니다.

그렇다면 앞으로 이런 일을 어떻게 방지할 수 있을까요? 현실적으로는 아마 불가능할지도 모르지만, 여러분 쪽에서 확률을 높이기 위해 할 수 있는 몇 가지 방법은 있습니다. 가장 좋은 방법은 pnpm 11 이상 버전을 사용하는 것입니다. 이 버전에는 이번 웜이 대부분의 사용자에게 도달하는 것을 막을 수 있었던 두 가지 기능이 기본적으로 활성화되어 있습니다. 첫 번째는 최소 출시 연령(minimum release age)입니다. 이는 pnpm이 게시된 지 24시간이 지나지 않은 모든 패키지를 거부하도록 설정하는 것으로, 대부분의 악성 패키지가 탐지되어 레지스트리에서 삭제되는 시점보다 충분히 긴 시간을 확보해 줍니다. 두 번째는 이색 하위 의존성(exotic subdependencies) 차단입니다. 보통 프로젝트의 모든 간접 의존성(transitive dependency)은 npm 레지스트리에서 가져오지만, 특정 패키지가 무작위 Git 저장소나 공격자의 S3 버킷에 있는 tarball URL을 가리키는 의존성을 목록에 올리는 것을 막을 방법은 없기 때문입니다.

이러한 이례적인 하위 의존성 (subdeps)을 차단합니다. 적절한 레지스트리 (registry)를 통하지 않은 것은 설치를 거부하며, 이는 악성 코드가 몰래 침투하는 가장 정교한 방법 중 하나를 차단합니다.

그리고 우리가 사용 중인 세 번째 pnpm 기능은 승인된 빌드 (approved builds)입니다. 대부분의 npm 악성 코드는 npm install을 입력할 때 자동으로 실행되는 설치 스크립트 (install scripts)를 통해 피해를 입힙니다. pnpm 11은 기본적으로 이 모든 것을 차단하며, 사용자가 의존성을 살펴보고 실제로 코드를 실행하도록 허용할 몇 개의 패키지를 화이트리스트 (whitelist)에 추가할 수 있게 합니다.

하지만 pnpm을 사용하더라도 프로덕션 (production) 환경에서는 여전히 문제가 발생할 수 있으며, 바로 이 지점에서 오늘의 스폰서인 Sentry가 도움을 줄 수 있습니다. 그들은 최근 사용자를 대신해 프로덕션 문제를 조사하는 AI 에이전트인 Sentry agent를 출시했습니다. 따라서 다음에 수많은 새로운 에러에 대한 Slack 알림을 받고 깜짝 놀라더라도, 대시보드와 로그를 직접 일일이 뒤지는 대신 Sentry agent에게 문제를 조사해 달라고 요청하기만 하면 됩니다. 이 에이전트는 Sentry가 이미 시스템에 대해 가지고 있는 모든 컨텍스트 (context)를 사용하여 트레이스 (traces), 스팬 (spans), 로그 (logs), 그리고 리플레이 (replays)를 가져옵니다. 그런 다음 서비스의 상류 (upstream)를 따라가며 실제 근본 원인 (root cause)을 찾아냅니다. 거기서부터 여러분은 Seer와 함께 수정 사항을 초안하고 풀 리퀘스트 (pull request)를 생성할 수 있으므로, 에러 메시지를 쳐다보는 시간은 줄이고 실제 세상에서 휴식을 취하는 (touch grass) 시간은 늘릴 수 있습니다. 개발자들은 이를 '자가 치유 소프트웨어 (self-healing software)'라고 부르고 있으며, 지금 바로 century.io/fireship에서 오픈 베타 기간 동안 무료로 체험해 볼 수 있습니다.

지금까지 Code Report였습니다. 시청해 주셔서 감사하며, 다음 영상에서 뵙겠습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 YouTube Fireship (개발 트렌드)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0