Skip to content
Published on

npm 공급망 공격이 사라지지 않는 이유 — npm-scan이 실제로 하는 일과, 정말 효과 있는 방어

공유하기
Authors

들어가며 — npm 공급망 공격은 예외가 아니라 구조다

npm 공급망 침해 소식은 이제 뉴스라기보다 배경 소음에 가깝다. 유명 패키지가 탈취되고, 설치만 했는데 토큰이 새어 나가고, 며칠 뒤 또 다른 패키지에서 같은 일이 반복된다. 이게 우연의 연속이 아닌 이유는, npm의 설계 자체가 공격에 넓은 표면을 내주기 때문이다. 이 글은 최근 Hacker News에서 화제가 된 npm-scan이라는 도구를 실마리로, 그 표면이 정확히 어디인지, 그리고 도구로 막을 수 있는 부분과 없는 부분이 어디까지인지를 정직하게 짚는다.

먼저 분명히 해 둘 것. npm-scan의 README는 자신감이 넘치고, 검증하기 어려운 수치를 앞세운다. 나는 그 도구가 겨냥하는 문제는 진짜라고 보지만, 그 수치를 그대로 받아들이지는 않는다. 문제와 도구를 나눠서 보자.

왜 npm은 구조적으로 취약한가

세 가지가 겹친다. 첫째, 설치 스크립트다. npm 패키지는 preinstall, install, postinstall 훅에서 임의의 코드를 실행할 수 있고, 그 코드는 설치를 실행한 사용자의 권한으로 돈다. npm install 한 번이 곧 신뢰하지 않는 코드의 실행이라는 뜻이다. 둘째, 전이 의존성이다. 당신이 직접 고른 패키지가 열 개라도 node_modules에는 수백 개가 들어오고, 당신은 그 수백 명의 유지보수자를 전부 신뢰하기로 이미 서명한 셈이다. 셋째, 타이포스쿼팅이다. lodashlodahs처럼 한 글자 차이의 이름은 오타 한 번으로 악성 패키지를 끌어온다.

각각은 개별적으로는 관리할 수 있다. 위험한 건 이것들이 조합된다는 점이다. 타이포스쿼팅된 이름이 패키지를 끌어오고, 그 패키지의 postinstall이 설치 시점에 돌고, 그것이 당신이 신뢰한 무언가의 전이 의존성으로 딸려 들어왔기에 아무도 검토하지 않는다. 세 약점은 별개의 위험 셋이라기보다 하나의 파이프라인이다.

이게 이론이 아니라는 걸 2025년 Shai-Hulud 웜이 보여 줬다. Palo Alto Unit42의 분석에 따르면, 이 웜은 설치 스크립트로 실행되어 훔친 npm 토큰으로 레지스트리에 로그인하고, 같은 개발자가 유지보수하는 다른 패키지에 악성 코드를 심어 스스로 번식했다. 표적은 .npmrc의 npm 토큰, GitHub PAT, 그리고 AWS·GCP·Azure의 API 키였다. 11월의 "Shai-Hulud 2.0"은 실행 시점을 postinstall에서 preinstall로 옮겼는데, Unit42의 표현으로는 이것이 "사람의 개입을 완전히 없애 사실상 모든 빌드 서버에서 실행을 보장"했다. 2.0은 수만 개의 GitHub 저장소에 걸쳐, 약 350명의 사용자에게서 2만 5천 개가 넘는 악성 저장소를 만들어 냈다.

이 셋 밑에는 네 번째 요인이 있다. 유지보수자 계정이다. 이런 캠페인의 대부분은 당신의 코드에서 버그를 찾아 시작되지 않는다 — 피싱당한 로그인이나 유출된 토큰으로 신뢰받는 게시자를 탈취한 뒤, 당신이 이미 의존하는 패키지의 악성 버전을 올리는 데서 시작된다. 정상 계정이 그것을 게시하는 순간, 위의 모든 메커니즘이 그것을 당신에게 자동으로 실어 나른다. 그래서 위험의 단위는 개별 패키지가 아니라 공급망 전체다.

핵심은 이 공격들이 특별한 제로데이를 쓴 게 아니라, npm이 의도적으로 제공하는 기능 — 설치 스크립트와 게시 권한 — 을 그대로 썼다는 점이다. 그래서 사라지지 않는다.

npm-scan이 실제로 검사하는 것 (과 검증할 수 없는 것)

npm-scan은 @lateos/npm-scan으로 배포되는, 대부분 JavaScript로 작성된 CLI 도구다. 표방하는 슬로건은 "npm audit, Snyk, Socket이 놓치는 것을 잡는다"이다. 사용법은 단순하다.

npm install -g @lateos/npm-scan
npm-scan axios                 # 단일 패키지 검사
npm-scan scan-lockfile         # 락파일 전체 검사
npm-scan express --json > findings.json

README가 내세우는 것은 "23개 탐지기(D1–D25)"를 통한 정적·행위 분석이다. 완전히 로컬에서 돌아 텔레메트리나 클라우드 의존이 없고, CI 실행당 30초 미만을 목표로 한다고 한다. 출력 쪽은 실무에 붙일 만하다 — SARIF(GitHub Security 연동), CycloneDX·SPDX SBOM 내보내기, YAML 정책 허용목록, GitHub Actions 통합. 겨냥하는 위협 범주도 이 글의 앞부분과 정확히 겹친다. 자격 증명 탈취, 난독화·안티디버깅, 메모리에서 OIDC·AI 키 추출, 그리고 웜 전파.

여기까지는 합리적이다. 문제는 README가 각 범주에 98%, 99% 같은 탐지율을 붙여 놓은 대목이다. 이 수치들에는 공개된 방법론도, 데이터셋도, 벤치마크도 없다. 어떤 표본에서 무엇을 참으로 놓고 잰 값인지 알 수 없으면, 그건 측정이 아니라 주장이다. 게다가 README에는 오탐(false positive)이나 한계에 대한 언급이 전혀 없다. 모든 스캐너는 오탐을 내고 놓치는 것이 있다. 강점만 나열하는 보안 도구는 그 자체로 경계 대상이다. 개별 탐지기 D1–D25가 무엇을 어떻게 잡는지도 README만으로는 알 수 없었다 — 상세 문서로 링크된 파일은 내가 확인한 시점에 열리지 않았다.

마지막으로 라이선스. 개인·오픈소스·평가용은 MIT로 무료지만, 프로덕션 사용에는 연 $799부터 $9,900까지의 상용 라이선스가 필요하다. CI에 상시로 물리기 전에 알아 둘 값이다.

표현에 관해 한마디 덧붙이자. "npm audit, Snyk, Socket이 놓치는 것을 잡는다"는 중립적 서술이 아니라 경쟁 주장이다 — Socket과 Snyk은 자체 행위 분석을 갖춘 성숙한 도구이고, '놓친다'는 단어가 많은 것을 떠안고 있다. '기존 도구가 못 잡는 걸 우리가 잡는다'는 식의 홍보는 결론이 아니라 당신의 의존성에 직접 시험해 볼 가설로 받아들이는 게 맞다.

그래서 정말 효과 있는 방어

스캐너는 방어의 한 겹일 뿐이고, 결정적인 공격자는 휴리스틱을 피해 간다. 블라스트 반경을 실제로 줄이는 건 지루한 기본기다.

  • 설치 스크립트를 끈다. .npmrcignore-scripts=true를 넣거나 npm install --ignore-scripts를 쓰면, Shai-Hulud류가 노리는 실행 지점 자체가 사라진다. 공짜는 아니다 — 네이티브 모듈처럼 빌드 스크립트가 꼭 필요한 패키지는 예외 처리해야 한다. 그래도 대부분의 의존성에는 스크립트가 필요 없다.
  • 락파일과 npm ci. package-lock.json을 커밋하고 CI에서는 npm install이 아니라 npm ci를 쓴다. 고정된 트리를 그대로 설치하고, 드리프트가 있으면 실패한다. PR에서 락파일 diff를 사람이 읽는 습관을 들이면, 예기치 않게 새로 들어온 전이 의존성이 눈에 띈다.
  • provenance를 검증한다. npm의 provenance/신뢰 게시(attestation)는 패키지를 그것을 빌드한 CI에 묶는다. npm audit signatures로 서명을 확인할 수 있다.
  • 의존성을 줄인다. 가장 근본적이다. 모든 전이 의존성은 공격 표면이고, 신뢰해야 할 유지보수자 한 명이다. 한 줄짜리 유틸을 위해 패키지를 더하기 전에, 그 표면을 늘릴 값어치가 있는지 묻는다.
  • 토큰을 관리한다. 빌드 머신에 .npmrc 토큰을 방치하지 않는다. 범위를 좁히고 주기적으로 회전한다. Shai-Hulud가 정확히 그 파일을 뒤졌다는 걸 기억하자.

이 목록은 대략 레버리지 순서다. 설치 스크립트 차단이 먼저인 이유는 그것이 최근 웜들이 지나간 바로 그 문을 닫기 때문이고, 의존성 줄이기가 마지막인 이유는 덜 중요해서가 아니라 효과가 가장 느리게 나타나기 때문이다 — 길게 보면 나머지 전부를 줄여 주는 건 그 항목이다.

npm-scan, Socket, Snyk 같은 스캐너는 이 기본기 위에 얹는 한 겹으로는 가치가 있다. 다만 스캐너가 위 목록을 대체하지는 못한다.

마치며

npm 공급망 공격이 사라지지 않는 이유는 그것이 버그가 아니라 기능의 오용이기 때문이다. 설치 스크립트와 게시 권한은 npm을 편리하게 만드는 바로 그 요소이고, 공격자는 그걸 의도대로 쓴다. 그래서 이 문제는 "완치"되지 않고 관리될 뿐이다.

그렇다고 비관할 일은 아니다. 궤적은 나빠지고 있다 — pre-install 실행과 자기 복제 웜은 분명한 격상이다 — 하지만 대응책은 잘 알려져 있고 대부분 공짜다. 부족한 건 지식이 아니라, 사고가 터지기 전에는 이 지루한 위생을 좀처럼 우선순위에 두지 않는다는 사실이다.

npm-scan 같은 도구를 대하는 태도도 같아야 한다고 본다. 그것이 겨냥하는 문제는 진짜이고, 로컬 실행·SARIF·SBOM 같은 설계는 합리적이다. 하지만 방법론 없는 탐지율과 한계에 대한 침묵은 신뢰가 아니라 검증을 요구한다. 스캐너는 한 겹으로 두고, 블라스트 반경을 실제로 줄이는 건 락파일과 스크립트 차단, provenance, 그리고 더 적은 의존성이라는 점을 잊지 않는 것 — 그게 정직한 결론이다.

참고 자료