Hardening npm dependency security

Alex O'Callaghan

npm 의존성 보안 강화하기

2026년 3월 30일, axios의 악성 버전 두 개가 npm에 잠시 게시되었습니다. axios는 주간 다운로드 수가 1억 건이 넘습니다. 공격자는 메인테이너 계정을 탈취해 [email protected][email protected]를 게시했으며, 각 버전에는 숨겨진 의존성이 포함되어 있었고 해당 의존성의 postinstall 훅이 크로스 플랫폼 원격 액세스 트로이목마를 몰래 설치하도록 했습니다.

악성 버전은 삭제되기 전까지 약 3시간 동안 유지되었으며, 마이크로소프트는 이번 공격의 배후로 북한 정부와 연계된 행위자를 지목했습니다. 이번 사건은 생태계에서 가장 널리 사용되는 패키지 중 하나를 노린, 치밀하게 준비된 표적형 공급망 공격이었습니다.

스스로를 보호하기 위해 실제로 무엇을 하고 있는지 점검해 볼 좋은 계기입니다.

당연한 것부터 시작하세요: lockfile 사용하기

당연한 이야기지만 그래도 짚고 넘어가겠습니다. lockfile을 커밋하세요. lockfile을 우회하는 방식으로 설치하지 마세요. lockfile이 있으면 오늘 semver 범위를 만족하는 아무 버전이나 가져오는 것이 아니라, 마지막에 리졸브된 그대로 정확히 설치하게 됩니다. 또한 공급망 관련 문제가 diff로 드러나기 때문에 전이 의존성이 예기치 않게 바뀌는 시점을 바로 알아챌 수 있습니다.

의존성 노출 범위를 줄이세요

공급망 공격에 대한 노출을 줄이는 가장 단순한 방법은 의존성을 줄이는 것입니다. 설치하지 않은 패키지는 탈취될 일도 없습니다.

먼저 사용하지 않는 의존성을 점검하세요. Knip은 코드베이스를 스캔해 package.json에는 등록되어 있지만 실제로는 어디에서도 import되지 않는 패키지를 찾아줍니다. 프로젝트에는 시간이 지날수록 쓰이지 않는 의존성이 쌓이기 마련이지만, 대부분 팀은 이를 적극적으로 정리하지 않습니다. Knip을 주기적으로 실행하거나 CI 파이프라인에 포함하면 불필요한 의존성을 제거할 수 있습니다.

다음으로 e18e를 살펴보세요. e18e는 자바스크립트 패키지를 정리하고 현대화하며 성능을 개선하는 데 초점을 둔 생태계 이니셔티브입니다. 이 활동의 일환으로 의존성으로 존재할 이유가 없는 is-odd 같은 패키지나 이제는 네이티브로 제공되는 lodash 함수 등을 더 가볍고 현대적인 대안으로 교체하는 작업이 이루어지고 있습니다. 의존성의 무게가 가벼워질수록 신경 써야 할 패키지도 줄어듭니다.

pnpm 설정

axios RAT는 패키지 설치 시 자동으로 실행되는 postinstall 훅을 통해 유포되었습니다. 이는 대부분의 npm 공급망 공격에 사용되는 방식입니다.

pnpm v10은 기본적으로 의존성의 postinstall 스크립트 자동 실행을 비활성화합니다. 요청하는 모든 패키지의 빌드 스크립트를 실행하는 대신, 정말 필요한 경우에만 명시적으로 허용 목록에 추가하는 방식입니다.

# pnpm-workspace.yaml
allowBuilds:
  esbuild: true
  "@parcel/watcher": true

이전에 빌드 스크립트가 필요하지 않았던 의존성은 갑자기 스크립트를 실행하지 않습니다. 해당 훅이 허용 목록에 없었다면, 탈취된 버전의 패키지라도 postinstall 훅을 이용해 악성 코드를 실행할 수 없습니다.

보안을 위해 다음 pnpm 설정들도 활성화하는 것이 좋습니다.

minimumReleaseAge: 10080 # 7 days in minutes
trustPolicy: no-downgrade
blockExoticSubdeps: true

minimumReleaseAge는 지정된 시간(분) 이내에 게시된 패키지 버전의 설치를 거부하도록 pnpm에 지시합니다. axios 공격은 3시간 동안 유지되었습니다. 하루(1440분)만 지연시켜도 이를 완전히 피할 수 있었을 것입니다. 저희는 Renovate의 안정성 윈도우에 맞춰 7일을 사용합니다.

trustPolicy: no-downgrade를 설정하면, 이전에 신뢰할 수 있는 CI 파이프라인의 provenance attestation과 함께 게시된 패키지의 새 버전에서 해당 증빙이 빠졌을 경우 pnpm이 설치를 차단합니다. 악성 버전이 정상 릴리스에 존재하던 trusted publisher 바인딩 없이 게시되었기 때문에, axios 공격도 이 방식으로 탐지할 수 있었습니다.

한 가지 주의할 점은 정상 패키지의 메인테이너가 provenance attestation을 중단하면 no-downgrade에서 간혹 오탐이 발생할 수 있다는 것입니다. 직접 검증한 특정 패키지는 trustPolicyExclude를 사용해 예외 처리할 수 있습니다.

trustPolicyExclude:
  - "[email protected]"

또한 blockExoticSubdeps: true도 명시적으로 추가하세요. 이렇게 하면 전이 의존성이 git 저장소나 직접적인 tarball URL로부터 리졸브되는 것을 막고, 반드시 레지스트리를 통해서만 가져오도록 강제할 수 있습니다.

내부 패키지는 스코프를 지정하세요

내부 패키지를 프라이빗 레지스트리에 게시한다면, 스코프가 없는 이름보다는 조직 스코프(예: @myorg/package-name) 아래에 두세요. 이렇게 하면 공격자가 내부 패키지와 동일하거나 유사한 이름의 공개 패키지를 게시하는 dependency confusion 공격 위험을 줄일 수 있습니다. 레지스트리 설정이 원상 복구되거나 새 환경이 공개 레지스트리를 우선하도록 잘못 구성될 경우, 스코프가 없는 내부 패키지 이름은 손쉬운 공격 대상이 됩니다.

Renovate로 자동 업데이트하기

저희는 프로젝트 전반의 의존성 업데이트를 관리하기 위해 Renovate를 사용합니다. 여기서는 두 가지 설정이 함께 동작합니다.

minimumReleaseAge(이전 명칭 stabilityDays)는 새 패키지 버전이 게시된 후 지정된 일수가 지날 때까지 Renovate가 PR을 생성하지 않도록 지연시킵니다. 저희는 이를 7일로 설정했습니다. 이를 통해 악성 릴리스가 코드베이스에 들어오기 전에 커뮤니티가 이를 발견할 시간을 벌 수 있으며, 부수적으로는 릴리스 후 이틀 만에 패치가 나오는 경우처럼 불필요하게 업데이트를 반복하는 것도 방지할 수 있습니다.

보안 업데이트 PR은 minimum release age를 완전히 우회합니다. Renovate가 의존성에서 알려진 취약점을 감지하면, 수정 버전이 게시된 지 얼마나 되었는지와 관계없이 즉시 PR을 생성합니다. 안정성 지연은 CVE 대응을 늦추지 않고, 일반적인 버전 업데이트만 늦춥니다.

내부 패키지의 경우 안정성 지연이 없는 별도의 package rule을 구성하세요. 통합 문제를 조기에 발견하려면 내부 패키지는 빠르게 롤아웃하는 것이 좋습니다.

{
  "minimumReleaseAge": "7 days",
  "packageRules": [
    {
      "matchPackagePrefixes": ["@myorg/"],
      "minimumReleaseAge": "0 days"
    }
  ]
}

어느 것도 완벽한 해결책은 아닙니다

이러한 설정은 직접 무언가를 탐지하기보다는 시간을 벌어주는 방식으로 동작합니다. 저희는 탈취된 패키지를 식별하기 위해 보안 연구원, 자동 스캔 플랫폼, 오픈소스 커뮤니티, 그리고 신속하게 패키지를 제거해 주는 npm 레지스트리 팀에 의존합니다. 안정성 윈도우는 다른 누군가가 문제를 찾아낼 때만 효과가 있으며, 추가하는 의존성이나 가져오는 업그레이드에 대해 경계를 늦추지 않는 것을 대체할 수는 없습니다.

원문은 Alex O'Callaghan님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.