3P by GN⁺ 4일전 | ★ favorite | 댓글 1개
  • NPM 저장소에서 100개 이상 자격 증명 탈취용 악성 패키지가 8월 이후 탐지되지 않은 채 업로드되어, 총 8만6천 회 이상 다운로드된 사실이 확인됨
  • 보안업체 Koi는 ‘PhantomRaven’으로 명명한 공격 캠페인이 NPM의 Remote Dynamic Dependencies(RDD) 기능을 악용해 126개의 악성 패키지를 배포했다고 보고
  • RDD는 패키지가 신뢰되지 않은 도메인에서 동적으로 의존성 코드를 내려받을 수 있게 하는 구조로, 정적 분석 도구에서는 탐지되지 않음
  • 공격자는 이 기능을 이용해 HTTP 연결을 통한 악성 코드 다운로드를 수행했으며, 패키지 메타데이터에는 “0 Dependencies”로 표시되어 개발자와 보안 스캐너가 인식하지 못함
  • 이러한 구조적 취약점은 NPM 생태계의 보안 관리 한계와 자동 설치 메커니즘의 위험성을 드러냄

NPM 저장소 내 악성 패키지 확산

  • 공격자들이 NPM 코드 저장소의 구조적 약점을 이용해 8월 이후 100개 이상의 자격 증명 탈취용 패키지를 업로드
    • 대부분의 패키지가 탐지되지 않은 채 배포되었으며, 누적 다운로드 수는 86,000회 이상
  • 보안업체 Koi는 이 공격을 PhantomRaven 캠페인으로 명명하고, NPM의 특정 기능이 악용되었다고 분석
    • Koi에 따르면 126개의 악성 패키지 중 약 80개가 기사 작성 시점에도 여전히 NPM에 남아 있었음

Remote Dynamic Dependencies(RDD)의 취약 구조

  • RDD는 패키지가 외부 웹사이트에서 의존성 코드를 동적으로 다운로드하도록 허용하는 기능
    • 일반적으로 의존성은 NPM의 신뢰된 인프라에서 내려받지만, RDD는 HTTP 등 암호화되지 않은 연결을 통한 다운로드도 허용
  • PhantomRaven 공격자는 이 기능을 이용해 악성 URL(예: http://packages.storeartifact.com/npm/unused-imports)에서 코드를 내려받도록 설정
    • 이러한 의존성은 개발자와 보안 스캐너에 보이지 않으며, 패키지 정보에는 “0 Dependencies”로 표시됨
  • NPM의 자동 설치 기능으로 인해 이러한 ‘보이지 않는’ 의존성 코드가 자동 실행되는 구조

보안 도구의 탐지 한계

  • Koi의 Oren Yomtov는 “PhantomRaven은 기존 보안 도구의 사각지대를 정교하게 악용한 사례”라고 언급
    • RDD는 정적 분석 도구에서 탐지되지 않음
  • 이로 인해 공격자는 보안 검증을 우회하며 악성 코드를 배포할 수 있었음

추가적 취약 요인

  • Koi는 RDD로 내려받은 의존성이 매 설치 시마다 공격자 서버에서 새로 다운로드된다고 설명
    • 캐시나 버전 관리가 없어, 동일 패키지라도 설치 시점마다 다른 악성 코드가 주입될 가능성 존재
  • 이러한 동적 다운로드 구조는 패키지 무결성 검증을 어렵게 함

NPM의 구조 및 배경

  • NPM은 JavaScript용 패키지 관리자로, GitHub 자회사인 npm, Inc. 가 관리
    • Node.js의 기본 패키지 관리자이며, 명령줄 클라이언트와 npm registry로 구성
    • registry에는 공개 및 유료 비공개 패키지가 저장되며, 웹사이트를 통해 검색 가능
  • 이번 사건은 NPM의 자동 의존성 관리 구조가 공격에 악용될 수 있음을 보여주는 사례로 지적됨

기타 언급

  • 기사 말미에서는 불필요한 JavaScript 실행을 차단해야 한다는 의견이 언급됨
    • 그러나 이번 공격은 필수 JavaScript 코드조차 악용된 사례로 지적됨
Hacker News 의견
  • 요즘 나는 npm 명령어를 Docker 컨테이너 안에서 실행하도록 alias를 걸어둠
    이렇게 하면 내 환경 변수를 노출하지 않고, 현재 디렉토리 외부 파일에도 접근하지 않으며, .bashrc 같은 설정 파일에도 접근하지 못하게 됨
    참고: Run tools inside Docker

    • 그건 너무 과한 샌드박싱 같음. 어차피 npm은 바로 실행될 임의 코드를 다운로드하니까
      대신 pnpm을 추천함. 기본적으로 lifecycle 스크립트를 실행하지 않고, 허용할 스크립트를 화이트리스트로 지정할 수 있음
    • post-install 스크립트를 악마화하는 건 잘못된 안전 환상을 줄 뿐임
      진짜 보호를 원한다면 설치뿐 아니라 실행 전체를 샌드박스 안에서 돌려야 함
      지금처럼 post-install만 막는 건 절반짜리 대책에 불과함. 공급망 공격은 점점 더 위험해지고 있음
    • 공격 벡터는 너무 많음. 악의적인 의도를 가진다면 인기 있는 플러그인이나 LSP 이름을 오타 스쿼팅해서 에디터 실행 시 자동으로 코드가 실행되게 만들 수 있음
      neovim이나 vscode가 감염되면 이미 사용자 권한으로 충분히 위험한 일들을 할 수 있음
    • 나는 sandbox-run을 사용함
      단순 alias는 node/npm에는 통하지만, 다른 프로그램들에는 적용하기 어려움. 컨테이너에 필요한 리소스를 마운트해야 하니까
    • 그래도 결국 악성 패키지를 다운로드할 수 있지 않음? 의존성 자체가 감염되어 있을 수도 있음
  • 예전부터 궁금했음. 왜 사람들은 아무렇지 않게 npm을 시스템에서 돌리는지
    make처럼 재현 가능한 빌드에 익숙한 입장에서는, npm이 매번 다른 걸 다운로드하고 다른 결과를 내는 게 충격이었음
    CSS 생성조차 npm 의존성으로 묶는 게 이상했음. 그래서 Docker 안에 npm 환경을 통째로 고정(freeze) 시켜봤지만, 결국 지는 싸움 같음

    • 요즘 모든 패키지 매니저가 그런 식으로 동작함. maven, nuget, pip, npm 모두 마찬가지임
      과거처럼 배포판 패키지 매니저에 의존하면 지금 같은 빠른 생태계는 불가능했을 것임
      다만 보안이 강화된 새 패키지 매니저들이 등장하고 있음. 이유를 이해하지 못한 채 수단만 비난하는 건 옳지 않음
    • 프론트엔드 개발은 마치 “믿어줘 브로”식 와일드 웨스트 같음. 브라우저 진화 과정상 어쩔 수 없이 덕트테이프처럼 이어붙인 느낌임
    • npm을 Docker로 고정했다면, 매번 의존성 업데이트 후 그 환경을 검증했는지 묻고 싶음
      사실 npmpnpm은 이미 기본적으로 lock 파일로 의존성을 고정함
    • npm install thing”이 너무 쉽고 싸서 생긴 문제임
      많은 오픈소스가 품질보다 이력서용 코드로 채워지고, 결국 거대 기업의 광고 추적기나 지갑 앱 같은 걸 만드는 데 쓰임
  • npm install은 단순히 패키지를 다운로드하는 게 아니라 코드를 실행
    package.json의 preinstall, install, postinstall 훅이 실제로 실행됨
    합법적으로 설치 과정에서 임의 명령을 실행해야 할 이유가 뭘까?
    관련 보고서: PhantomRaven npm malware
    또 다른 사례: Socket.dev 블로그

    • 사실 이런 구조는 오래된 패키지 매니저(DEB, RPM)에도 있었음
      예를 들어 리눅스 커널 패키지는 설치 후 initramfs 재생성, GRUB 업데이트 등을 위해 post-install 스크립트를 실행함
      대부분의 DEB/RPM 패키지에 이런 스크립트가 포함되어 있음. 즉, 설계 자체의 문제임
    • 문제는 npm아무나 패키지를 올릴 수 있다는 점
      리눅스 배포판은 신뢰할 수 있는 메인테이너 체계가 있고, PGP 기반의 루트 트러스트를 직접 구축하기도 함
      반면 npm, pip, rubygems, cargo 등은 사실상 “curl | bash”의 세련된 버전일 뿐임
    • 예를 들어 Mediasoup 프로젝트는 C++로 작성된 스트리밍 라이브러리인데, 설치 시 소스를 직접 컴파일함
      유지보수 부담을 줄이기 위해 이런 post-install 빌드가 필요했음
    • Swift Package ManagerPackage.swift 파일을 실제로 실행함
      다만 강하게 샌드박싱되어 있어서 악용은 어렵다고 들음
      참고: SwiftPM 문서, PackageDescription
    • 참고로 pnpm v10은 기본적으로 모든 lifecycle 스크립트를 비활성화하고, 사용자가 직접 허용해야 함
      관련 논의
  • 최근 npm 공격들을 보면, 이제는 npm으로 개발하는 게 안전한가 싶음
    React 프로젝트를 시작할 때마다 수백 개의 패키지가 깔리는데, 뭘 하는지도 모름
    백엔드에서는 필요한 패키지만 명시적으로 설치하지만, 프론트엔드는 취약점의 판도라 상자 같음

    • 사실 모든 언어 생태계가 비슷함. 단지 npm이 제일 크고 뉴스에 많이 나올 뿐임
    • Rust의 jj를 설치했더니 470개, Python의 wan2gp는 211개 패키지가 깔렸음. 다 거기서 거기임
    • JavaScript 생태계는 구조적으로 공격에 취약함
      xz 사건처럼, 각 의존성이 무작위 개인에게 달려 있고, 그들이 사회공학 공격에 당하지 않을 거라 믿어야 함
    • 의존성이 적을수록 좋음. 0개면 완벽임. 그게 진짜 승리임
    • 참고로 PyPI도 안전하지 않음. GitHub Actions 해킹으로 정상 패키지에 악성 코드가 삽입된 사례도 있음
  • Angular, Vue 같은 프레임워크로 개발할 때마다 불안함
    node_modules 안의 수천 개 의존성을 보면 재앙의 예고처럼 느껴짐
    오픈소스 개발자 한 명이 피싱당하면 바로 감염될 수도 있음
    JavaScript 생태계는 근본적으로 깨져 있음. 오타 한 번으로 공급망 공격에 노출됨
    NuGet이나 Maven도 가능은 하지만, 그쪽은 표준 라이브러리가 커서 의존성이 적고 통제감이 있음

    • Go는 패키지 이름 대신 repo URL을 사용해서 오타 스쿼팅을 줄임
      완벽하진 않지만 그래도 한 단계 나음
    • Deno는 이런 문제를 해결함. Node.js / npm의 구조적 문제
  • 86,000회 다운로드 중 대부분은 실제 사용자가 아니라 자동화된 스캐너나 봇일 가능성이 큼
    새 버전을 올리면 하루이틀 만에 수백 번 다운로드되지만, 실제 사람은 아닐 수도 있음
    즉, 감염된 사용자는 거의 없을 수도 있음

    • 나도 라이브러리를 올렸을 때 초반엔 주당 300회, 이후엔 100회 정도 다운로드됐음
      AI 챗봇이 환각으로 만들어낸 패키지 이름을 노린 공격도 많음. 단순한 통계 이상임
    • 혹은 누군가의 좀비 CI가 계속 다운로드하는 걸 수도 있음
    • 하지만 LLM이 만들어낸 가짜 패키지 이름을 노린 공격이라면, 실제로 많은 개발자가 감염됐을 수도 있음
  • 더 자세한 공격 설명은 BleepingComputer 기사 참고

  • npm installHTTP URL을 의존성으로 사용하는 패키지를 탐지하거나 필터링할 방법이 있을까 궁금함
    요청자별로 다른 페이로드를 보낼 수 있어서 일반적인 스캐너로는 탐지가 어려움

  • 취미 개발자로서 이런 공급망 공격에 대비하려면 어떻게 해야 할지 고민임
    유명한 튜토리얼을 따라가며 의존성을 설치하다 보면, 어느새 보안에 무심해짐
    내 홈랩에도 여러 서비스를 돌리는데, 혹시라도 봇이 침투할까 걱정임. 어디서부터 시작해야 할까?

    • 컨테이너나 VM 안에서 서비스를 분리해 돌리면 피해를 격리할 수 있음
      완벽한 보장은 아니지만, 전체 서버가 뚫리는 것보단 훨씬 낫음
    • 모든 의존성을 잠재적 보안 리스크로 보고, 꼭 필요할 때만 사용해야 함
      필요한 코드만 직접 복사해 쓰는 것도 좋은 학습이자 안전한 방법임
    • 인기 있고 1년 이상 된 릴리스를 사용하는 게 안전함. 문제가 있었다면 이미 발견됐을 확률이 높음
    • FreeBSD처럼 시스템 패키지 매니저를 사용하는 OS도 있음
      이런 구조라면 수백만 사용자가 직접 검증할 필요 없이 배포판 수준에서 신뢰성 확보가 가능함
    • 주간 다운로드 100만 회 이상, 의존성이 없는 패키지를 선호함
      예: Hono, Zod
      나는 최근 Bun으로 전환했는데, 기본적으로 DB 드라이버나 S3 클라이언트 같은 게 내장되어 있어 추가 다운로드가 줄어듦
  • lifecycle 훅에서 의존성을 가져오는 구조는 언제든 공격 전환점이 될 수 있음
    지금은 정상이라도, 나중에 소유자가 해킹당하거나 마음을 바꾸면 악성 코드로 바뀔 수 있음
    이런 형태의 설치 훅은 결국 지속 불가능한 설계