npm 의존성 혼동 공격을 막는 레지스트리 운영 원칙
사내 패키지명, 프라이빗 레지스트리, lockfile 검증을 묶어 의존성 혼동 공격 가능성을 줄이는 방법을 정리했다. npm·Dependency Confusion·Supply Chain 관점에서 기본 개념부터 구현 순서, 검증 방법, 문제가 생겼을 때 되돌리는 절차까지 설명한다.
핵심 요약
의존성 혼동은 단순히 이름을 잘못 짓는 문제가 아니다. 사내 패키지 이름이 공개 레지스트리에서도 해석되고, CI가 더 높은 버전이나 예상 밖의 소스를 받아들이며, 설치 스크립트가 빌드 권한으로 실행될 때 발생한다. 조직 스코프, 레지스트리 라우팅, lockfile 고정, 설치 권한 분리를 하나의 통제로 운영해야 한다.
npm 의존성 혼동 공격은 공격자가 사내 패키지와 같은 이름을 공개 레지스트리에 등록하고, 빌드 시스템이 내부 패키지 대신 공격자 패키지를 내려받게 만드는 공급망 공격이다. 원인은 패키지 관리 도구 하나가 아니라 이름 해석, 레지스트리 우선순위, 버전 선택, lockfile 갱신, 설치 스크립트 실행 권한이 연결된 운영 방식에 있다.
평소 개발자 노트북에서는 프라이빗 레지스트리 캐시 덕분에 정상 동작하다가, 깨끗한 CI 러너나 신규 프로젝트에서만 공개 레지스트리로 빠지는 경우도 있다. 따라서 “우리 환경에서는 재현되지 않는다”는 말은 안전 증거가 아니다. 동일한 설정으로 비어 있는 캐시에서 재현하고, 실제 다운로드 출처를 확인해야 한다.
공격이 성립하는 전형적인 경로
다음 조건이 겹치면 위험이 커진다.
- 내부 패키지가
payments-utils처럼 스코프 없는 이름을 사용한다. - 프록시 레지스트리가 내부에 없으면 공개 npm 레지스트리로 폴백한다.
- 공개 레지스트리에 같은 이름의 더 높은 버전이 존재한다.
- lockfile이 없거나 CI에서 설치 중 다시 생성된다.
- 패키지의
preinstall,install,postinstall스크립트가 허용된다. - CI 토큰이 소스 저장소, 배포 환경, 클라우드 자격 증명에 접근할 수 있다.
공격자는 복잡한 익스플로잇이 없어도 설치 시점에 환경 변수와 파일을 읽거나 외부로 연결을 시도할 수 있다. 패키지가 빌드 결과물에 포함되지 않더라도, 설치 단계에서 실행됐다면 이미 사고로 보아야 한다.
조직 스코프를 신뢰 경계로 사용한다
사내 패키지는 @회사명/패키지명처럼 조직이 통제하는 스코프 아래 배치하는 것이 좋다. 그리고 해당 스코프를 프라이빗 레지스트리에 명시적으로 연결한다.
@acme:registry=https://registry.internal.example/npm/
//registry.internal.example/npm/:_authToken=${NPM_TOKEN}
registry=https://registry.npmjs.org/
이 예시는 방향을 보여 주기 위한 것이다. 실제 URL과 인증 방식은 조직 환경에 맞춰 검증해야 한다. 중요한 것은 @acme/* 요청이 공개 레지스트리로 조용히 폴백하지 않도록 레지스트리 또는 프록시 정책에서도 강제하는 일이다.
스코프만 붙이고 공개 레지스트리에 같은 스코프 패키지를 발행할 권한을 방치하면 충분하지 않다. npm 조직 소유권, 패키지 생성 권한, 멤버 변경, 복구 이메일, 게시 계정의 피싱 저항 MFA를 함께 관리한다.
lockfile은 출처까지 검증해야 한다
package-lock.json을 커밋하고 CI에서는 npm install 대신 npm ci를 사용하면 선언 파일과 lockfile이 다를 때 설치가 실패한다. 그러나 lockfile이 존재한다는 사실만으로 안전하지는 않다. 코드 리뷰와 자동 검사에서 다음 항목을 확인한다.
- 새 패키지의
resolved호스트가 승인된 레지스트리인지 - 내부 스코프가 공개 레지스트리 URL을 가리키지 않는지
integrity값이 갑자기 사라지거나 대량 변경되지 않았는지- 잠금 파일 버전이 도구 업그레이드 없이 바뀌지 않았는지
- 직접 의존성이 아닌 전이 의존성에서 새 설치 스크립트가 추가됐는지
- 레지스트리 미러가 원본 무결성 메타데이터를 보존하는지
자동 의존성 업데이트 봇도 동일한 정책을 통과해야 한다. “봇이 만든 PR”은 신뢰 근거가 아니라 변경 주체를 구분하는 메타데이터일 뿐이다.
설치와 게시 권한을 분리한다
개발자와 CI 대부분은 읽기 전용 설치 권한만 있으면 된다. 패키지 게시 권한은 별도 워크플로와 승인된 저장소에만 부여한다.
| 작업 | 권한 원칙 | 권장 증거 |
|---|---|---|
| 일반 설치 | 읽기 전용, 짧은 수명 토큰 | 러너 ID, 레지스트리, 패키지 목록 |
| 내부 게시 | 지정 저장소와 브랜치에서만 | 커밋 SHA, 빌드 실행, 승인자 |
| 공개 게시 | 별도 승인과 조직 소유권 확인 | provenance, 서명, 배포 기록 |
| 긴급 폐기 | 제한된 관리자 역할 | 사고 티켓, 폐기 사유, 후속 검토 |
장기 NPM_TOKEN을 여러 저장소에 복사하기보다 지원되는 환경에서는 OIDC 기반 trusted publishing 같은 짧은 수명 자격 증명을 검토한다. 토큰이 필요하다면 환경별로 분리하고, 패키지 게시와 배포 권한을 한 토큰에 묶지 않는다.
설치 스크립트와 네트워크를 통제한다
설치 스크립트를 전부 금지하면 네이티브 모듈 등 정상 빌드가 깨질 수 있다. 현실적인 방법은 기본 차단과 명시적 허용 목록을 조합하는 것이다.
- 신규 패키지는 우선
--ignore-scripts환경에서 설치 가능 여부를 확인한다. - 스크립트가 필요한 패키지는 이유, 소유자, 허용 버전을 기록한다.
- 빌드 러너의 외부 네트워크 목적지를 레지스트리와 필수 서비스로 제한한다.
- 설치 단계에는 운영 비밀값과 배포 자격 증명을 주입하지 않는다.
- 빌드와 배포를 서로 다른 작업과 서비스 계정으로 분리한다.
- 패키지 캐시는 신뢰할 수 있는 빌드 결과만 승격하고, 캐시 키에 lockfile 해시를 포함한다.
2026년 6월 기준 npm 11 문서의 npm approve-scripts와 npm deny-scripts는 검토한 설치 스크립트 정책을 package.json에 기록하는 데 유용하지만, 현재 릴리스에서는 정책 정보가 아직 권고 성격이며 미검토 스크립트를 자동 차단하지 않는다. 따라서 이 설정만으로 집행이 끝났다고 보지 말고
--ignore-scripts 기반 검증, 러너 네트워크 제한, 자격 증명 분리, CI 정책 검사를 함께 사용한다. npm을 업그레이드할 때는 해당 버전의 스크립트 집행 동작을 공식 문서에서 다시 확인한다.
이 통제는 AI 모델 공급망과 같은 대용량 아티팩트에도 동일하게 적용할 수 있다. 출처가 불명확한 파일을 운영 워크로드가 실행 시점에 직접 내려받지 않도록 한다.
탐지 신호
다음 이벤트를 빌드 로그와 레지스트리 감사 로그에서 탐지한다.
- 내부 스코프가 공개 npm 도메인으로 조회된다.
- 처음 보는 패키지 이름이나 게시자가 추가된다.
- 같은 패키지의 다운로드 출처가 프라이빗에서 공개로 바뀐다.
- lockfile 없이 설치하거나 CI에서 lockfile이 수정된다.
- 설치 단계에서 승인되지 않은 외부 도메인으로 DNS·HTTP 연결이 발생한다.
- 설치 스크립트가 홈 디렉터리, SSH 키, 클라우드 메타데이터에 접근한다.
- 게시 토큰이 평소와 다른 저장소·IP·시간대에서 사용된다.
- 내부 패키지와 같은 이름이 공개 레지스트리에 새로 등록된다.
경보에는 패키지명과 버전만 넣지 말고 저장소, 커밋, 러너, 레지스트리 URL, 요청한 사용자, 실행된 스크립트를 함께 제공한다.
자주 실패하는 운영 방식
| 실패 모드 | 왜 위험한가 | 개선 방법 |
|---|---|---|
| 내부 패키지가 스코프 없는 이름 사용 | 공개 레지스트리와 이름 충돌 | 조직 스코프와 예약 정책 적용 |
| 프록시가 모든 미존재 패키지를 공개로 폴백 | 내부 이름 오타도 외부 조회 | 내부 스코프는 폴백 금지 |
개발자마다 .npmrc가 다름 | CI와 로컬 결과가 달라짐 | 저장소·조직 표준 설정과 정책 검사 |
npm install로 CI 실행 | lockfile이 조용히 변경될 수 있음 | npm ci와 변경 실패 처리 |
| 게시 토큰을 빌드 전 과정에 제공 | 악성 설치 스크립트가 탈취 가능 | 설치·게시·배포 자격 증명 분리 |
| 패키지명만 검토 | 전이 의존성과 스크립트 누락 | lockfile diff와 행위 기반 검사 |
의심 패키지를 발견했을 때
- 해당 패키지를 사용한 빌드와 배포를 일시 중지한다.
- 패키지 tarball, lockfile, 레지스트리 응답, 빌드 로그를 보존한다.
- 설치 시점에 노출됐을 수 있는 토큰과 세션을 폐기한다.
- 네트워크 로그에서 설치 스크립트의 외부 통신을 추적한다.
- 승인된 버전과 해시로 lockfile을 복원하고 깨끗한 러너에서 재빌드한다.
- 이미 배포한 산출물은 동일 패키지가 포함되지 않았더라도 빌드 단계 침해 가능성을 평가한다.
- 공개 레지스트리 신고, 내부 패키지 이름 예약, 프록시 폴백 정책을 보완한다.
- 관측성 데이터 개인정보 보호 원칙에 따라 조사 로그에 비밀값이 다시 노출되지 않게 한다.
운영 체크리스트
- 모든 내부 패키지가 조직 스코프를 사용한다.
- 내부 스코프는 프라이빗 레지스트리에만 해석된다.
- 깨끗한 캐시 환경에서 레지스트리 라우팅을 시험했다.
- lockfile을 커밋하고 CI에서
npm ci를 사용한다. - lockfile의
resolved호스트와 무결성 변경을 자동 검사한다. - 설치 스크립트가 필요한 패키지는 허용 목록과 소유자가 있다.
- 설치 러너에는 운영 배포 자격 증명이 없다.
- 게시 권한은 지정된 저장소와 워크플로에만 있다.
- 내부 이름의 공개 레지스트리 등록을 주기적으로 감시한다.
- 의심 패키지 발견 시 토큰 폐기와 재빌드 절차를 시험했다.
참고 기준
- npm Docs: Scopes
- npm Docs: package-lock.json
- npm Docs: npm ci
- npm Docs: Trusted Publishing
- npm Docs: Generating Provenance Statements
- npm Docs: npm approve-scripts
- npm Docs: npm deny-scripts
- SLSA Specification v1.2
결론
의존성 혼동 방어의 핵심은 “공개 패키지를 조심하자”가 아니다. 내부 이름은 내부 레지스트리에서만 해석하고, 설치 대상과 출처를 lockfile로 고정하며, 설치 과정이 탈취할 수 있는 권한을 최소화하는 것이다. 세 통제가 함께 있어야 한 번의 이름 충돌이 빌드 시스템 전체 침해로 번지는 것을 막을 수 있다.
전체 댓글 0개