1P by GN⁺ 22시간전 | ★ favorite | 댓글 3개
  • Shai-Hulud 2.0 악성 npm 패키지가 개발자 머신을 감염시켜 Trigger.dev의 GitHub 조직 접근권을 탈취한 사건 발생
  • 감염은 개발자가 pnpm install 실행 시 악성 패키지의 preinstall 스크립트가 실행되며 시작, TruffleHog 도구를 이용해 자격 증명 탈취
  • 공격자는 17시간 동안 669개 저장소를 복제하고, 이후 10분간 199개 브랜치에 강제 푸시 및 42개 PR 폐쇄 시도
  • 패키지와 프로덕션 시스템은 손상되지 않았으며, 공격은 4분 만에 탐지되어 계정 접근이 차단됨
  • 사건 이후 npm 스크립트 비활성화, pnpm 10 업그레이드, OIDC 기반 npm 배포, 브랜치 보호 전면 적용 등 보안 체계 강화

공격 개요

  • 2025년 11월 25일, 내부 Slack 디버깅 중 Linus Torvalds 명의의 “init” 커밋이 여러 저장소에 생성되는 이상 징후 발생
  • 조사 결과, Shai-Hulud 2.0 공급망 웜이 개발자 머신을 감염시켜 GitHub 자격 증명을 탈취한 것으로 확인
  • 이 웜은 500개 이상 npm 패키지를 감염시키고, 25,000개 이상의 저장소에 영향을 미친 것으로 보고됨
  • Trigger.dev의 공식 npm 패키지(@trigger.dev/*, CLI)는 감염되지 않음

공격 타임라인

  • 11월 24일 04:11 UTC: 악성 패키지 배포 시작
  • 20:27 UTC: 독일의 개발자 머신 감염
  • 22:36 UTC: 공격자 첫 접근 및 대량 저장소 복제 시작
  • 15:27~15:37 UTC (11월 25일): 10분간 파괴적 공격 수행
  • 15:32 UTC: 이상 탐지 및 4분 내 접근 차단
  • 22:35 UTC: 모든 브랜치 복구 완료

감염 과정

  • 개발자가 pnpm install 실행 시, 악성 패키지의 preinstall 스크립트가 실행되어 TruffleHog을 다운로드 및 실행
  • TruffleHog은 GitHub 토큰, AWS 자격 증명, npm 토큰, 환경 변수 등을 스캔 후 외부로 유출
  • 감염된 머신에서 .trufflehog-cache 디렉터리와 관련 파일이 발견됨
  • 감염 원인 패키지는 삭제되어 추적 불가

공격자의 활동

  • 감염 후 17시간 동안 정찰 활동 지속
    • 미국과 인도 기반 인프라를 이용해 669개 저장소 복제
    • 개발자 활동을 모니터링하며 GitHub 토큰을 이용한 접근 유지
    • “Sha1-Hulud: The Second Coming”이라는 이름의 저장소 생성, 자격 증명 저장에 사용된 것으로 추정
  • 이후 10분간 파괴적 행위 수행
    • 16개 저장소에서 199개 브랜치 강제 푸시 시도
    • 42개 PR 폐쇄, 일부는 브랜치 보호 설정으로 차단
    • 모든 커밋은 “Linus Torvalds <email> / init” 형태로 표시

탐지 및 대응

  • Slack 알림을 통해 이상 징후를 실시간 탐지
  • 4분 내 감염 계정의 GitHub 접근 차단, 이후 AWS·Vercel·Cloudflare 등 모든 서비스 접근 철회
  • AWS CloudTrail 로그 분석 결과, 읽기 전용 API 호출만 존재, 프로덕션 데이터 접근 없음
  • AWS는 별도로 Shai-Hulud 관련 의심 행위를 탐지해 경고 발송

피해 및 복구

  • 669개 저장소 복제, 199개 브랜치 강제 푸시, 42개 PR 폐쇄
  • GitHub의 서버 측 reflog 부재로 복구 어려웠으나, 이벤트 API와 로컬 reflog를 활용해 7시간 내 전체 복원
  • npm 패키지 및 프로덕션 인프라는 손상되지 않음

GitHub App 키 노출

  • 조사 중 개발자 노트북의 휴지통에서 GitHub App 개인 키 발견
  • 해당 키는 고객 저장소에 read/write 권한을 가지며, 즉시 회전 조치
  • 데이터베이스(설치 ID 저장)는 손상되지 않아 고객 저장소 접근 증거 없음, 그러나 완전 배제는 불가
  • GitHub 지원팀에 추가 로그 요청 및 고객에게 이메일 공지 발송

Shai-Hulud 기술 분석

  • setup_bun.js 실행 시 Bun 런타임을 설치하고 백그라운드에서 bun_environment.js 실행
  • TruffleHog을 이용해 $HOME 디렉터리 내 자격 증명 수집
  • 수집된 데이터(contents.json, cloud.json, truffleSecrets.json 등)를 랜덤 GitHub 저장소에 3중 base64 인코딩 형태로 업로드
  • npm 토큰이 존재할 경우, 감염된 계정의 패키지를 수정·재배포하여 웜 확산
  • 자격 증명이 없을 경우 홈 디렉터리 삭제 시도
  • 감염 지표 파일: setup_bun.js, bun_environment.js, .trufflehog-cache/

보안 강화 조치

  • npm 스크립트 전면 비활성화 (ignore-scripts=true)
  • pnpm 10 업그레이드: 스크립트 기본 비활성화, minimumReleaseAge(3일) 설정으로 신규 패키지 설치 지연
  • OIDC 기반 npm Trusted Publishers 도입으로 장기 토큰 제거
  • 모든 저장소에 브랜치 보호 적용
  • AWS SSO에 Granted 도입, 세션 토큰 암호화
  • GitHub Actions에서 외부 기여자 워크플로 실행 시 승인 필수로 변경

다른 팀을 위한 교훈

  • npm 설치 시 실행되는 임의 코드 실행 구조 자체가 공격 표면
  • ignore-scripts=true 설정 및 필요한 패키지만 화이트리스트 관리 필요
  • pnpm minimumReleaseAge로 신규 패키지 설치 지연
  • 브랜치 보호OIDC 기반 배포는 필수 보안 조치
  • 로컬 머신에 장기 자격 증명 저장 금지, CI를 통한 배포만 허용
  • Slack 알림의 소음이 탐지의 열쇠가 되었음

인간적 측면

  • 감염된 개발자는 잘못이 없으며, 단순히 npm install 실행만으로 피해 발생
  • 공격 중 해당 계정이 수백 개의 무작위 저장소를 자동으로 ‘star’한 흔적 발견
  • 사건은 개인의 실수가 아닌 생태계의 구조적 취약성을 드러냄

요약 지표

  • 최초 감염 후 첫 공격까지: 약 2시간
  • 공격자 접근 유지 시간: 17시간
  • 파괴 행위 지속 시간: 10분
  • 탐지까지 5분, 차단까지 4분
  • 전체 복구 완료까지 7시간
  • 복제된 저장소: 669개 / 영향받은 브랜치: 199개 / 폐쇄된 PR: 42개

참고 리소스

  • Socket.dev: Shai-Hulud Strikes Again V2
  • PostHog, Wiz, Endor Labs, HelixGuard의 분석 보고서
  • npm Trusted Publishers, pnpm onlyBuiltDependencies, minimumReleaseAge, Granted 문서

pnpm은 기본값으로 post-install을 개별적으로 허용해야하는 구조였을텐데 결국 개발자도 무의식적으로 허용하게 되나봅니다

npm은 기본값으로 실행하게 되어있어서 pnpm으로 전환해서 기본 비활성 전환하여 보안을 강화한 것으로 이해됩니다.

Hacker News 의견들
  • npm install을 실행하는 건 태만이 아님
    문제는 패키지 설치 과정에서 임의 코드 실행을 허용하는 생태계
    하지만 근본적인 보안 실패는, 제3자가 아무 검증 없이 내 제품에 코드를 밀어넣을 수 있는 패키지 매니저를 사용하는 것임
    결국 우리는 패키지 관리자와 그 운영자들의 선의와 역량에 무한히 의존하고 있음
    또 OP가 자격 증명을 평문으로 파일 시스템에 저장했다고 암시하는 것 같음

    • 두 가지 모두 문제라고 생각함
      언어 차원에서 코드가 입력을 읽고, 자원을 소비하고, 타입이 올바른 출력만 생성하도록 제한하는 구조를 만들 수 있음
      완전한 공급망 문제 해결은 아니지만, 노출 범위는 훨씬 줄어듦
    • 이런 논리는 너무 순환적임
      “한 사람이 이런 도구를 쓰는 건 잘못이 아니지만, 모두가 쓰면 생태계가 문제다”라는 식임
      이미 많은 개발 도구가 보안 취약하다는 건 여러 번 증명된 사실임
      진짜로 신경 쓴다면, 행동으로 보여야 함
    • IDE 플러그인도 마찬가지임
      VS Code를 쓸 때 작은 기능 하나 추가하려고도 누가 만든지도 모르는 플러그인을 설치해야 해서 불편했음
    • 제3자 코드 실행을 막는 패키지 매니저를 어떻게 설계할 수 있을지 궁금함
      결국 신뢰할 수밖에 없는 코드를 실행하게 되는 구조 아닌가 생각함
    • 일부 도구는 http 인증을 위해 netrc 파일만 지원함
      git을 http로 쓰면 이런 평문 자격 증명 노출 경로는 거의 항상 존재함
  • pnpm 메인테이너가 1년 전에 “post-install 스크립트를 기본 차단하자”고 제안했음
    사용자 입장에서는 불편하겠지만, 장기적으로는 모두가 감사하게 될 변화라 믿었음
    관련 PR: pnpm/pnpm#8897

    • 그런데도 여전히 같은 문제가 반복되고 있음
      결국 편의성이 보안을 이긴 또 한 번의 사례임
  • “데이터베이스는 침해되지 않았다”고 했지만, 공격자가 AWS와 시크릿에 접근했다면 이미 침해된 것이라 봄
    접근 가능성이 있었다면 침해로 간주해야 함

    • AWS 리소스 접근 로그가 있고, 권한 회수 전 접근 흔적이 없다면 데이터는 안전하다고 볼 수 있음
  • 악성코드가 실행된 후에는 출처 추적이 거의 불가능
    pnpm install도 정상적으로 완료되니 탐지하기 어려움
    Sentinel One이나 CrowdStrike 같은 EDR이 있었다면 조사 단서가 더 많았을 것임

    • EDR이 있었다면 Trufflehog 공격 체인을 탐지하거나 차단했을 가능성이 높음
  • “총 669개의 repo를 클론했다”는 부분이 눈에 띔
    직원 수가 100명도 안 되는 회사가 600개 넘는 repo를 가진 게 정상인지 궁금함

    • 완전히 정상임. repo는 가축이지 반려동물이 아님
    • 우리 조직은 7명인데 GitHub에 365개 repo가 있음
      팀 규모보다 연차와 프로젝트 수명이 repo 수에 더 큰 영향을 줌
    • 마이크로서비스를 좋아하는 아키텍트가 있으면 이런 결과가 나옴
  • pnpm은 이미 preinstall 같은 lifecycle 스크립트 자동 실행을 중단했는데, 구버전을 썼던 것 같음
    관련 PR 참고

    • 기사 마지막에 최신 메이저 버전으로 업데이트했다고 언급함
    • 그게 pnpm을 쓰는 주된 이유라 생각했는데 혼란스러움
    • 결국 구버전 패키지 매니저로 의존성 업데이트를 한 셈이라면, 이건 그들의 책임임
    • 프로젝트 자체에 postinstall 스크립트가 있었을 수도 있음
      pnpm은 의존성의 스크립트는 막지만, 프로젝트 레벨 스크립트는 여전히 실행함
  • 투명한 사후 분석(post-mortem) 공유에 감사함
    이런 사례가 업계 전체에 중요함
    공격 트래픽이 일반 개발 트래픽과 구분 가능했는지 궁금함
    우리도 dev 환경에서 egress 필터링을 강화하려 하지만 npm install이 자주 깨져서 고민임

    • GitHub 조직의 IP 허용 목록 기능을 쓰면 이런 공격 방어에 도움이 될 듯함
  • 개인 노트북의 git 보안을 고민 중임
    현재는 SSH 키를 로컬에 두고 push함
    관리자 권한도 있어서 위험함. 더 안전하게 만들 방법이 궁금함

    • 1Password를 써서 SSH 키와 Git 서명을 관리하고, GitHub OAuth나 CLI 로그인으로 push/pull 하는 구성을 추천함
      이렇게 하면 서명 키와 접근 키를 분리할 수 있고, 관리자 계정은 별도로 관리 가능함
      관련 문서: 1Password SSH Agent, Git Commit Signing, GitHub OAuth, GitHub CLI Login
    • 나는 SSH 개인키를 TPM에 저장해서 PKCS11로 SSH agent에서 사용함
      키가 외부로 유출될 수 없다는 장점이 있지만, 악성코드가 내 머신에서 동작하면 여전히 위험함
      Linux 예시, macOS 예시
    • 노트북이 감염되면 방어 수단이 없음
      TPM이나 Yubikey로 조금 어렵게 만들 수는 있지만, 완전한 방어는 불가능함
      관리자 작업은 별도 전용 머신에서 하는 게 안전함
    • Yubikey에 GPG 키를 넣고 gpg-agent로 SSH 인증을 하는 방법도 있음
      push나 commit 시 PIN 입력으로 잠금 해제함
    • main 브랜치 직접 push를 막고, MFA를 필수화하면 공격자가 바로 배포 브랜치에 접근하기 어려움
  • Torvalds 이름의 커밋은 감염 후 흔히 보이는 표식(signature) 이었음
    Microsoft의 공식 분석 글에서도 언급됨
    이 웜은 매우 시끄러웠고, 일부 공격자는 노출된 자격 증명으로 비공개 repo를 공개하거나 readme를 수정해 홍보용으로 악용했음

  • 공격자가 파괴 행위 없이 조용히 정보만 유출했다면, 이런 탐지가 가능했을지 의문임