3P by GN⁺ 8시간전 | ★ favorite | 댓글 1개
  • NPM 생태계에서 인기있는 @ctrl/tinycolor를 포함한 40개 이상 패키지에 자기 전파형 악성코드가 주입되어, 개발 환경의 비밀정보와 CI/CD 자격증명까지 연쇄 감염될 수 있는 공급망 공격이 발생. 감염 버전은 npm에서 제거되었음
  • 공격 페이로드는 npm 설치 과정에 Webpack 번들(bundle.js, ~3.6MB) 을 비동기로 실행하고, 환경변수·파일시스템·클라우드 SDK를 통해 광범위한 크리덴셜 수집을 수행함
  • 악성 로직은 NpmModule.updatePackage로 다른 패키지를 강제 패치·배포해 캐스케이딩 전파, GitHub Actions에 shai-hulud 워크플로를 주입해 조직 시크릿을 toJSON(secrets) 로 탈취함
  • 수집 데이터는 공개 GitHub 저장소 ‘Shai-Hulud’ 생성으로 수행되어 유출되며, 정상 개발 활동으로 위장되며 탐지 회피성이 높음
  • AWS/GCP/Azure/NPM/GitHub 토큰 및 메타데이터 엔드포인트 접근, TruffleHog 기반 비밀 탐색 등으로 은밀히 수행됨
  • 즉시 패키지 제거·리포지토리 정리·전 크리덴셜 교체와 함께 CloudTrail/GCP Audit 로그 점검, 웹훅 차단, 브랜치 보호/Secret Scanning/쿨다운 정책 도입이 요구됨

Affected Packages

  • 총 195개 패키지/버전이 보고됨, 대표적으로 @ctrl/tinycolor(4.1.1, 4.1.2), @ctrl/ 네임스페이스 다수, @crowdstrike/ 모듈군, ngx-bootstrap/ngx-toastr/ng2-file-upload/ngx-colorAngular/웹 UI 생태계 전반, @nativescript-community/@nstudio/ 모바일 스택, teselagen/ 생명과학 툴체인, ember-*, koa2-swagger-ui, pm2-gelf-json, wdio-web-reporter 등이 포함됨
  • 각 패키지별 정확한 버전은 원문 표를 참고해 해당 버전 사용 여부를 정밀 교차 확인 필요
    • 예시: @ctrl/ngx-emoji-mart 9.2.1, 9.2.2, @ctrl/qbittorrent 9.7.1, 9.7.2, ngx-bootstrap 18.1.4, 19.0.3–20.0.5, ng2-file-upload 7.0.2–9.0.1 등 광범위

Immediate Actions Required

Identify and Remove Compromised Packages

  • 프로젝트에서 감염 패키지 존재 여부 확인: npm ls @ctrl/tinycolor 등으로 점검
  • 감염 패키지 즉시 제거: npm uninstall @ctrl/tinycolor 등 수행
  • 알려진 bundle.js 해시 검색으로 로컬 흔적 점검: sha256sum | grep 46faab8a... 사용

Clean Infected Repositories

  • malicious GitHub Actions 워크플로 삭제: .github/workflows/shai-hulud-workflow.yml 제거함
  • 원격에 생성된 shai-hulud 브랜치 탐지·삭제: git ls-remote ... | grep shai-huludgit push origin --delete shai-hulud 수행함

Rotate All Credentials Immediately

  • NPM 토큰, GitHub PAT/Actions 시크릿, SSH 키, AWS/GCP/Azure 자격증명, DB 연결 문자열, 서드파티 토큰, CI/CD 시크릿전면 교체 필요
  • AWS Secrets Manager/GCP Secret Manager에 저장된 항목 포함 전체 회전 필요

Audit Cloud Infrastructure for Compromise

  • AWS: CloudTrail에서 BatchGetSecretValue, ListSecrets, GetSecretValue 호출 시점과 패턴 점검, IAM Credential Report로 비정상 키 생성·사용 확인
  • GCP: Audit LogsSecret Manager 접근 기록 점검, CreateServiceAccountKey 이벤트 존재 여부 확인
Hacker News 의견
  • npm에서 호스팅되는 패키지를 사용하는 입장에서, 모든 의존성과 그 의존성의 의존성까지 직접 감시하는 것은 현실적인 방법이 아님을 느낌, 타입스크립트/자바스크립트 전문가도 아니어서 공격자가 숨겨놓은 악성코드를 쉽게 발견할 수 없을 거라 생각함, 최근 고민은 '지연 모드'로 업데이트하는 방법임, 즉, 최신 버전이 아니라 일정 기간 이상 지난 버전으로만 업데이트하는 방식인데, 패키지가 6주 정도 외부에 노출되어 있으면 악성코드가 드러났을 확률이 높다고 보는 방식임, 하지만 완벽한 방법은 아니고, 보안 이슈가 있는 경우엔 예외적으로 바로 최신 업데이트를 적용할 수도 있게 옵션을 주는 도구가 있었으면 좋겠음

    • 기사에서 바로 언급된 방법으로, NPM Package Cooldown Check라는 기능이 있음, 조직에서 설정한 기간(기본 2일) 이내에 릴리즈된 패키지 버전이 pull request에 추가되면 자동으로 빌드가 실패함, 대다수 서플라이 체인 공격이 24시간 이내에 탐지되니, 아주 짧은 대기만으로도 보안 노출을 줄일 수 있음

    • 모든 의존성 전체를 검사하는 게 힘드니, 가능한 한 의존성 수를 줄이고, 잘 알려지고 신뢰할 만한 패키지만 쓰라는 주장을 하고 싶음, 오히려 모든 저자를 신뢰할 만큼 통제된 환경이 아닌 이상 'not-invented-here'를 어느 정도 유지하는 게 상식적인 선택임

    • package.json에서 버전을 명시적으로 고정(pin)해두고, npm ci를 사용해 package-lock.json에 명시된 버전만 설치하는 습관이 있음, CI에서 npm audit을 실행해 패키지에 취약점이 나타나면 알람을 받음, 이렇게 하면 패키지는 거의 '동결' 상태가 되고, 패키지의 나이 자체만으로도 감염 가능성이 떨어짐

    • 나의 경우엔 이보다 더 나가서 버그가 실제로 내 사용 환경에 영향을 끼칠 때만 의존성을 업데이트하고 있음, 보안 취약점이 있어도 영향을 안 주면 그냥 넘어감, 대부분의 개발자는 불필요하게 자주 의존성을 업데이트하지만, 실제로 필요한 경우만 하는 게 맞다고 생각함, 만약 업데이트가 자주 필요하거나 복잡하다면 그 패키지는 아예 안 쓰거나, 내 기준으로 '동결'함

    • 파이썬의 uv를 활용하면 비슷한 형태로 업데이트를 제한할 수 있음, 예를 들어 uv lock --exclude-newer $(date --iso -d "2 days ago")와 같은 커맨드로 2일 이내에 릴리즈된 버전은 제외할 수 있음

  • 새로운 패키지나 버전이 감시받지 않기 때문에 이런 문제가 발생하는 것임, Debian처럼 오로지 보안 패치와 버그픽스만 적용되는 안정된 배포판과, 패키지 메인테이너가 감시하는 testing/unstable 배포판을 분리해 운용하는 게 최선임, 중앙 집중형 오픈 패키지 저장소(NPM, Python, Rust 등) 종사자 모두 같은 문제를 겪고 있음

    • 개발자 문화에 문제가 있음, 수백 개의(전이적) 의존성을 두고, 아무 생각 없이 자동 업데이트하는 문화 자체가 문제임, 그렇게 수많은 타사 코드를 빌드/실행 환경에 노출하는 선택엔 책임이 따름

    • 배포판(디스트로)도 유지 관리해야 하는 패키지의 양에 점점 더 부담을 느낌, 사실 이런 이유로 언어별 생태계(예: CPAN, Maven, RubyGems 등)가 발전했음, 리눅스 배포판만으로는 사용자가 원하는 앱을 제공하기 어려워서 freshmeat, linuxbrew, flatpak, PPA 등 다양한 경로가 생김, 모든 커뮤니티가 수많은 다양한 라이브러리의 여러 브랜치를 감시하고 지원할 역량은 없다고 봄

    • Debian 개발자로서 업스트림 코드 반영 전에 점점 더 많은 '노이즈'(특히 단순한 스타일 변경, 툴링업데이트) 때문에 실제 변화 감지가 너무 어려워짐, 이런 변화는 진짜로 사람의 점검이 필요한 리팩토링, 버그 수정, 기능 추가, 또는 문제가 될만한 코드를 찾기 위한 툴링 결과가 아니라면 자제했으면 하는 바람임

    • Rust에서는 cargo vet이라는 시스템이 있음, 구글과 Mozilla 같은 회사들이 참여해 패키지를 자동으로 공유, 검증함

    • 탈중앙화도 유지하면서 어느 정도 안전장치를 두는 방법이 있다고 봄, 예를 들어 일정 규모 이상의 패키지는 2FA를 갖춘 두 개의 계정승인 필수, 또는 인기 패키지는 오직 재현 가능한 빌드 시스템으로만 npm 업로드 가능하게 하는 방안 등을 고민해볼 수 있음, 완전한 분산을 포기하지 않고, 대형 프로젝트에서만 약간 추가 수고가 필요한 정도임

  • 최근 계속되는 서플라이 체인 공격 때문에 서버 렌더링(자바스크립트 없이)을 더 진지하게 고려하게 됨, HTMX 덕분에 자바스크립트 없이도 정말 먼 곳까지 갈 수 있다는 걸 깨달음, 이렇게 하면 앱이 더 빠르고 안정적이기까지 할 것 같음

    • 전통적인 JS 환경은 실제로 가장 안전한 샌드박스 환경임을 강조하고 싶음, 거의 30년간 수십억 대의 디바이스에서 비신뢰 JS 코드가 실행되지만, 브라우저 엔진에서 대규모 공격이 성공한 사례는 손에 꼽을 정도임, 하지만 NodeJS와 npm 환경은 보안적으로 전면 재설계가 필요한 상태임, leftpad 같은 일들은 단순 코드 스니펫도 npm에 올리는 문화에서 기인함

    • 이런 공격이 특정 환경(자바스크립트)만의 이슈로 자동적으로 축소 논의되는 경향이 이상함, 사실 npm에조차 마련된 보안 강화책이 다른 (PyPI, Crates 등) 환경엔 전혀 적용되지 않는 점이 더 큰 문제로 보임

    • 벤더링으로 노출을 줄일 수 있지만 근본 문제 해결책은 아니라고 판단함, NPM이 보안을 진지하게 생각한다면, 퍼블리시에 2FA와 패키지 사전 스캔을 의무화하고 하드웨어 키로 서명까지 강제해야 함, semver나 CRC로는 부족함, 이 모든 게 패키지 관리 시스템 내에 기본 탑재되어야 함

    • 사실 이런 공격이 자바스크립트만의 문제가 아니라, 새 의존성을 추가할 때 개발자들이 충분히 감시하지 않는 데서 비롯됨, 이는 Rust나 Go와 같은 다른 언어 생태계에도 그대로 적용될 수 있음

    • 패키지 매니저에 크게 의존하고, 표준 라이브러리가 빈약한 언어는 모두 동일하게 취약함, 장기적으로는 바닐라 자바스크립트를 사용하는 방향으로 돌아가야겠다는 생각임, Rust도 동일하게 패키지 의존도가 높음, 오히려 Go가 이런 문제에선 표본적인 사례임

  • 신뢰할 수 있는 키로 커밋/릴리즈에 서명하고, 설치 및 검증까지 하는 경량 코드를 추적할 수 있는 시스템이 필요하다고 생각함, 이미 sigstore를 활용한 npm의 프로비넌스 방식이 있으나, 아직은 널리 쓰이지 않고 발행자 검증 정도에만 국한된 것으로 보임

  • 2016년에 이미 NPM에 해당 취약점이 보고됨 (CERT 권고), 그러나 NPM의 답변은 WAI(working as intended)였음

    • WAI가 무슨 뜻인지 모르는 분을 위해: 일반적으로 “working as intended”의 약자임

    • 혹시 postinstall 스크립트가 아예 없더라도, 빌드 과정, 서버 기동, 테스트 등에서 모듈을 임포트하면 결국 악성코드는 실행될 것 같음, 결국 npm install 후에 뭔가 실제로 돌리는 순간은 반드시 있으니...

  • left-pad 사태 때 여기서 본 코멘트가 떠오름, 저명한 npm 메인테이너가 600개 npm 패키지와 1,200줄 자바스크립트 코드를 갖고 있다는 얘기였음, 본보기로 꼽고 싶은 케이스는 esbuild임, 외부 의존성 거의 없고, Go 표준 라이브러리만을 사용하고 있음
    "차세대"라 불리는 다른 프로젝트들도 의존성 체인을 보면 biomejs, swc 도 상당히 적음, 다만 Rust 원본 코드를 보면 결국 biomejs, swc 역시 많은 의존성을 가짐, 이런 프로젝트가 퍼지면 cargo 생태계도 같은 전철을 밟을 거라 예상함, 혹시 esbuild처럼 엄격한 스타일로 작성된 대형 프로젝트를 아는 분 있으면 추천해 주셨으면 함

    • Go로 넘어간 이유 중 하나가 purego 방식의 라이브러리 트렌드 때문임, 보통 표준 라이브러리와 golang.org/x만 의존하고, CGO 없이 컴파일 가능해서 휴대성이 뛰어남, go mod vendor로 단기적인 위험관리는 되지만 근본 해결책은 아님, Go 역시 패키지 검증(사인/키체크 등)이 엔드 투 엔드로 제공되지 않아 결국 취약점이 남음, 특히 CI/CD 인프라에 많이 집중돼 있는데, 서명 키 전달 없이 빌드, 배포가 가능하다면 보안성도 높일 수 있을 것으로 봄, 패키지 매니저는 GPG 서명을 장려하고, git 커밋에도 서명을 도입해 배포해야 한다고 생각함

    • eslint의 경우가 대표적으로 답답하게 느껴짐, 종속성 그래프를 보면 엄청나고, 유지보수자가 의존성 감축을 우선시하지 않으면 결국 다른 솔루션(oxlint)으로 갈아타는 방법 뿐임

    • 쉬운 기능은 자기가 직접 만들고 외부 의존성을 줄이는 것이 해답임, 보통 이런 식으로만 해도 전체 의존성의 2/3는 감축할 수 있음, 특히 left-pad 같이 간단한 건 직접 만들고, 작은 유닛과 테스트로 자기 손 안에 두는 것이 관리 부담도 그리 크지 않음, 불필요한 의존성은 과감히 배제해야 함

    • Rust 프로젝트의 root Cargo.toml에 잡혀있는 건 전체 워크스페이스(작업공간)용이고, 실제 각 crate(패키지)의 의존성은 훨씬 얕음, 더 깊이 들여다봐야 실제 종속성 구조를 알 수 있음

    • 단점은 자바스크립트 프로젝트 점검을 위해 이제 Golang도 읽어야 한다는 점임, 게다가 post-install로 또 node install.js가 실행되니, 결국 완전히 신뢰하거나 코드를 다 읽는 수밖에 없음

  • npm이 아직도 모든 의존성의 postinstall 스크립트를 기본적으로 실행한다는 게 믿기지 않음, Pnpm이나 Bun은 허용 목록에 등록된 경우에만 실행하고, Composer는 아예 라이프사이클 스크립트를 의존성에 대해 실행하지 않음, 빌드나 개발 환경에서 의존 패키지가 갖는 위험 때문에 이런 방식이 더 안전하다고 생각함

    • 이런 대규모 공격이 다른 패키지 매니저(예: Rust build.rs, 파이썬, 자바 등)에선 그리 자주 들리지 않는 이유가 궁금함, postinstall은 물론이고, 사실상 거의 모든 생태계에서 원리상 가능한데 npm 위주로만 사건이 집중되는 듯함

    • Pnpm의 기본값이 스크립트 차단으로 바뀐 것을 봤음, 커뮤니티 반응(스크립트 허용을 두고 사용 경험, allow 커맨드의 남용 등)이 궁금하고, 파이썬 패키징 커뮤니티에서도 wheel variants와 관련된 유사한 논의가 진행 중임, 다른 생태계의 경험을 참고하고 싶음

  • 이번 공격이 180개 이상의 패키지로 확산됨, Aikido Security 블로그 참고

  • 이 공격을 최초로 발견한 사람은 누구인지 궁금함, 여러 블로그마다 각기 공을 돌리는 방식이 달라 흥미로움, Aikido는 “우리가 대규모 공격을 발견했다”고 말하고, Socket, Ox, Safety, Phoenix, Semgrep 등도 각자 다르게 기술함

    • 나는 Aikido에서 일하는 Mackenzie임, 처음 이 사안을 보고한 사람은 개발자 Daniel Pereira로, 이 분이 Socket에 전달했고, Socket이 처음 40개 패키지와 악성코드를 분석했음, 이후 Aikido가 추가로 147개 패키지와 Crowdstrike 패키지까지 발견함, 실제로 Step이 처음으로 악성코드가 자기 전파형 웜임을 알아냈음, 여러 조직이 독립적으로 다양한 역할을 했다는 점이 재미있음

    • 여러 개발자가 거의 비슷한 시점에 발견한 것으로 보이며, Step과 Socket은 각각 다른 분을 언급함, 궁극적으로는 업계 보안 벤더들이 AI 코드 분석(Socket, Aikido)나 eBPF 파이프라인 모니터링(Step) 등 각자 방법으로 잡아냈음

    • 이렇게 많은 벤더가 독립적으로 감지했다면, 그 기술을 npm에 바로 공유하면 아예 악성 패키지 등록 자체가 차단될 수 있지 않았을까 하는 의문이 듬, 그럼 조기 경보고지 시스템을 벤더가 판매할 수 없게 되니 내놓지 않는 것 같음

    • OP 기사는 “@franky47이 이 현상을 발견후 곧바로 커뮤니티에 GitHub 이슈로 알렸음”이란 문구를 직접 인용함

  • 공격자가 던 이름 'Shai Hulud'가 꽤 재치 있다고 생각함, 거대 벌레의 이름을 실제 웜 악성코드에 붙임, 핵심 bundle.js도 3.6MB로 거대함, 변종 악성코드조차 npm스럽게 덩치가 매우 커져버렸음

    • 조만간 어느 한 공급망 공격이 또다른 공급망 공격을 우연히 끌어오는 일도 생길 거라 예감함

    • 악성코드 역시 무어의 법칙을 따름, 1991년 tequila virus는 2.6KB였는데 지금은 수 MB임