npm 의존성 보안 강화하기
원문은 Alex O'Callaghan님이 에 게재했습니다. 이 블로그 구독하기
2026년 3월 30일, axios의 악성 버전 두 개가 npm에 잠시 게시되었습니다. axios는 주간 다운로드 수가 1억 건이 넘습니다. 공격자는 메인테이너 계정을 탈취해 [email protected]과 [email protected]를 게시했으며, 각 버전에는 숨겨진 의존성이 포함되어 있었고 해당 의존성의 postinstall 훅이 크로스 플랫폼 원격 액세스 트로이 목마를 몰래 설치했습니다.
악성 버전은 삭제되기 전까지 약 세 시간 동안 유지되었으며, 마이크로소프트는 이번 공격의 배후로 북한 정부 연계 행위자를 지목했습니다. 생태계에서 가장 널리 사용되는 패키지 중 하나를 노린, 치밀하게 준비된 표적 공급망 공격이었습니다.
자신을 보호하기 위해 실제로 무엇을 하고 있는지 점검해 볼 좋은 계기입니다.
당연한 것부터 시작하자: 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: trueminimumReleaseAge는 지정된 시간(분) 이내에 게시된 패키지 버전의 설치를 거부하도록 pnpm에 지시한다. axios 공격은 세 시간 동안 유지됐다. 하루 지연(1440)만 설정했어도 이를 완전히 피할 수 있었을 것이다. 우리는 Renovate의 안정성 윈도우에 맞춰 7일로 설정해 사용한다.
trustPolicy: no-downgrade가 설정되면, 이전에 신뢰할 수 있는 CI 파이프라인의 provenance attestation과 함께 게시됐던 패키지의 새 버전이 해당 증빙 없이 게시된 경우 pnpm이 설치를 차단한다. axios 공격도 이런 방식으로 탐지할 수 있었다. 악성 버전들은 정상 릴리스에 존재하던 신뢰된 퍼블리셔 바인딩 없이 게시됐기 때문이다.
한 가지 주의할 점은, 정상 패키지의 메인테이너가 provenance attestation을 중단하면 no-downgrade가 간혹 오탐을 발생시킨다는 것이다. 직접 검증한 특정 패키지는 trustPolicyExclude를 사용해 예외 처리할 수 있다:
trustPolicyExclude:
- "[email protected]"blockExoticSubdeps: true도 명시적으로 추가하라. 이 설정은 전이 의존성이 git 저장소나 직접적인 tarball URL로부터 해석되는 것을 방지하고, 반드시 레지스트리에서 가져오도록 강제한다.
내부 패키지에 스코프를 지정하라
프라이빗 레지스트리에 내부 패키지를 게시한다면, 스코프 없는 이름 대신 조직 스코프(예: @myorg/package-name) 아래에 두도록 하라. 이렇게 하면 공격자가 내부 패키지와 동일하거나 유사한 이름의 퍼블릭 패키지를 게시하는 의존성 혼란 공격 위험을 줄일 수 있다. 레지스트리 설정이 잘못되거나 새로운 환경이 퍼블릭 레지스트리를 우선 참조하도록 잘못 구성된 경우, 스코프가 없는 내부 패키지 이름은 손쉬운 공격 대상이 된다.
Renovate로 자동 업그레이드하기
우리는 프로젝트 전반의 의존성 업데이트를 관리하는 데 Renovate를 사용한다. 여기서는 두 가지 설정이 함께 작동한다.
minimumReleaseAge(이전 명칭 stabilityDays)는 새 패키지 버전이 게시된 후 지정된 일수가 지날 때까지 Renovate가 PR을 생성하지 않도록 지연시킨다. 우리는 이를 7일로 설정한다. 이를 통해 악성 릴리스가 우리 코드베이스에 반영되기 전에 커뮤니티가 이를 발견할 시간을 확보할 수 있으며, 부수적으로 이틀 뒤에 패치가 나오는 릴리스를 성급하게 가져와 발생하는 불필요한 churn도 방지할 수 있다.
보안 업데이트 PR은 minimum release age를 완전히 우회한다. Renovate가 의존성에서 알려진 취약점을 감지하면, 수정 버전이 얼마나 최근에 게시됐는지와 관계없이 즉시 PR을 생성한다. 안정성 지연은 CVE 대응을 늦추지 않으며, 일상적인 버전 업데이트만 늦출 뿐이다.
내부 패키지의 경우, 안정성 지연이 없는 별도의 패키지 규칙을 구성하라. 통합 문제를 조기에 발견하려면 내부 패키지를 빠르게 반영하는 것이 좋다.
{
"minimumReleaseAge": "7 days",
"packageRules": [
{
"matchPackagePrefixes": ["@myorg/"],
"minimumReleaseAge": "0 days"
}
]
}어느 것도 만능 해결책은 아니다
이러한 설정들은 무언가를 직접 탐지하기보다는 시간을 벌어주는 방식으로 작동한다. 우리는 탈취된 패키지를 식별하기 위해 보안 연구원, 자동화된 스캐닝 플랫폼, 오픈소스 커뮤니티, 그리고 이를 신속히 제거하는 npm 레지스트리 팀에 의존한다. 안정성 윈도우는 다른 누군가가 문제를 찾아낼 때만 효과를 발휘하며, 추가하는 의존성이나 가져오는 업그레이드에 대해 경계를 늦추지 않는 것을 대체할 수는 없다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기