Velog
NPM 보안 모범 사례
npm 공급망 공격은 늘었고, 도구는 늘 그 자리에 있었다. 핵심은 `.npmrc` 한 파일에 다섯 줄 만 정확히 적는 것이다.
핵심 요약
이 글은 bodadotsh/npm-security-best-practices 를 기반으로 14개 항목을 정리하지만, 실무 핵심은 개발자용 6개 항목이다. .npmrc 한 파일에 다음을 적어두는 것이 출발점이다.
ignore-scripts=true
provenance=true
save-exact=true
save-prefix=''
주요 관문 6개를 표로:
| # | 관문 | 명령/설정 | 효과 |
|---|---|---|---|
| 1 | 버전 고정 | npm install --save-exact / save-exact=true | semver 범위로 인한 자동 업그레이드 차단 |
| 2 | 잠금 파일 | npm ci, --frozen-lockfile | 환경 간 동일성 보장 |
| 3 | 라이프사이클 차단 | ignore-scripts=true | postinstall 류 악성 스크립트 무력화 |
| 4 | 최소 릴리스 기간 | pnpm: minimumReleaseAge, yarn: npmMinimalAgeGate | 갓 배포된 악성 버전 격리 |
| 5 | 권한 모델 | node --permission | 임의 코드 실행 시 자원 접근 제한 |
| 6 | 외부 의존 줄이기 | 내장 fetch, node:test, node --watch 등 | 공격 표면 자체 축소 |
메인테이너 측은 2FA(WebAuthn 권장), 세분화된 토큰, OIDC 신뢰 게시, files 화이트리스트 가 핵심.
npm 보안은 '스캐너 도입' 이 아니라 '설치 시점의 관문 5개' 로 본다. 버전 고정 → 잠금 파일 → 라이프사이클 차단 → 최소 릴리스 기간 → 외부 의존 줄이기. 이 다섯 관문이 공격의 평균 노출 시간(수 시간) 보다 길게 작용하면 대부분의 공급망 공격은 무력화된다.
npm 생태계의 공격 빈도 는 면접 단골이다.
event-stream(2018),ua-parser-js(2021),axios(2026/3) — 모두 메인테이너 토큰 탈취Shai-Hulud같은 worm 은postinstall스크립트로 자격 증명 탈취- left-pad 사고처럼 제거 만으로도 빌드가 멎음
좋은 답변은 도구 이름(npm audit, snyk)을 나열하는 게 아니라, "왜 이 다섯 관문이 그 순서인가" 를 설명하는 것이다. 이 글은 그 다섯을 한 묶음으로 정리해 둔 가장 컴팩트한 자료다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
다섯 관문의 *순서* 가 곧 비용 곡선
관문은 도입 비용이 다르다 — 무서운 게 아니라 순서대로 켜는 게 정답이다.
0원 비용: save-exact + lockfile → 즉시
낮은 비용: ignore-scripts → 빌드 깨질 때만 화이트리스트
중간 비용: minimumReleaseAge → 의존 최신화 워크플로 필요
높은 비용: node --permission → 코드 변경 동반
첫 두 줄은 .npmrc 만 고치면 끝나고, 효과는 공격의 90% 가 들어오는 자동 업그레이드 경로를 막는다. 가장 큰 ROI 다.
"보안 == 비싼 도구" 라고 착각하는 것. 실제로는 0원 짜리 줄 두 개가 가장 큰 효과를 낸다.
라이프사이클 스크립트 차단의 비대칭 효과
Shai-Hulud worm 같은 공격은 postinstall 한 줄로 자격 증명을 빼간다. 차단 비용 vs 효과의 비대칭이 매우 크다.
npm config set ignore-scripts true --global
yarn config set enableScripts false
# bun, deno, pnpm: 기본적으로 비활성화
CI 에는 더 강하게 — npm ci --omit=dev --ignore-scripts. 빌드가 정말로 postinstall 을 필요로 하는 소수 패키지 만 화이트리스트(예: pnpm onlyBuiltDependencies)로 풀면 된다. 효과/비용 비가 가장 좋은 방어다.
"라이프사이클 끄면 빌드 다 깨짐" 이라고 미리 포기하는 것. 실제로는 0~3개 패키지만 화이트리스트하면 충분한 경우가 대부분이다.
최소 릴리스 기간 — 시간으로 거른다
공격자는 토큰 탈취 후 수 시간 내 발각 전에 끝내야 한다(axios 4h, ua-parser-js 4h). "새 버전을 N일 격리" 는 이 노출 창을 그대로 이긴다.
# pnpm — 분 단위 (7일)
minimumReleaseAge: 10080
# yarn 4 — duration string
npmMinimalAgeGate: "7d"
# Renovate / Dependabot — 보안 패치는 우회
cooldown:
default-days: 7
semver-patch-days: 3
주의: 잠금 파일을 완전 고정 하는 팀에는 효과가 작다 — 관문은 업데이트 시점 에만 의미가 있다.
스캐너 도입을 우선시하는 것. 시간 격리는 스캐너로 못 잡는 제로데이 까지 시간으로 막는 패시브 방어다.
읽는 순서
- 1이론
GitHub
bodadotsh/npm-security-best-practicesREADME 를 정독하고, 14개 항목 중 우리 팀이 이미 켠 것/아직 안 켠 것 표로 만드세요. - 2구현
프로젝트에
.npmrc4줄(save-exact, ignore-scripts, save-prefix='', provenance) 을 추가하고npm ci --ignore-scripts로 CI 가 그대로 도는지 검증. 깨지는 패키지를 화이트리스트 로 푸세요. - 3실무
Renovate 또는 Dependabot 에 cooldown 7일을 적용하되 보안 업데이트는 0일 우회. PR 한두 개로 실제 동작 확인.
- 4설명
팀에 5분 발표 — '5개 관문(고정/잠금/스크립트/시간/권한) + 운영 비용'. 각 관문이 막는 대표 공격 사례 를 한 줄씩.
면접 연결 질문
[감점 답변] 'audit 돌린다'. [좋은 답변] ignore-scripts=true. 비용 0, 효과 비대칭 — postinstall 류가 가장 흔한 자격 증명 탈취 경로이고, 빌드가 진짜 postinstall 을 필요로 하는 패키지는 손에 꼽는다. 화이트리스트로 풀면 끝.
[감점 답변] '잠금이 있으면 충분하다'. [좋은 답변] 잠금은 이전에 본 버전 만 보호한다. 새 버전을 추가/업그레이드하는 순간 (npm install / Renovate PR) 다시 0일 패키지가 들어온다. 따라서 (1) save-exact 로 자동 업그레이드 차단, (2) minimumReleaseAge 로 신상 격리 가 추가로 필요하다. 잠금은 재현성 이지 방어 가 아니다.
[감점 답변] '되는 데까지 켠다'. [좋은 답변] 내부 패키지는 하루 안에 여러 번 게시 되므로 7일 게이트가 항상 막는다. 해결: scope allowlist (@org/* 는 minimumReleaseAge 0), 또는 사설 레지스트리에서 업스트림만 게이트 적용. CVE 패치는 cooldown 우회(Renovate osvVulnerabilityAlerts: true) — 보안 패치는 0일이어야 한다.
자기 점검
"전부 한 번에 켜자" — 실제로는 비용 낮은 것부터 1주 단위로 도입해야 빌드 회귀를 잡을 수 있다.
TOTP 만 켜면 안전하다는 가정. 실제로는 WebAuthn(보안 키) + OIDC 신뢰 게시 가 토큰 자체를 없앤다.
유틸 라이브러리가 의존 트리를 폭발시키지 않는다는 인식. 작은 라이브러리도 transitive 가 길면 공격 표면을 키운다.