본문 바로가기
C.W.K.
Stream
Lesson 01 of 05 · published

공급망 보안 — npm 의존성도 실행 코드야

~13 min · production, security, supply-chain, npm-audit

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Node 앱에서 실행되는 코드 대부분은 직접 쓰지 않은 의존성이야. npm install은 편의 명령인 동시에 신뢰할 코드를 고르는 결정이야."

의존성이 늘면 공격 표면도 넓어져

애플리케이션 코드는 몇천 줄이어도 설치된 node_modules에는 수백 개 패키지와 그보다 많은 간접 의존성이 들어갈 수 있어. 각 패키지는 설치 스크립트나 import 시점의 코드로 실행될 수 있고, 애플리케이션과 같은 권한으로 파일과 네트워크, 하위 프로세스에 접근할 수도 있어.

대부분의 패키지는 그런 권한을 악용하지 않지만 npm 생태계에는 비슷한 이름을 이용한 위장 패키지, 관리 계정 탈취, 인기 패키지의 악성 업데이트가 실제로 있었어. 직접 의존성 하나를 고를 때 그 아래에 딸려 오는 코드와 설치 동작까지 함께 신뢰한다는 사실을 잊으면 안 돼.

매일 지킬 다섯 가지

1. 잠금 파일을 커밋해. package-lock.json이나 pnpm-lock.yaml은 간접 의존성까지 버전과 무결성 값으로 고정해.

2. CI에서는 잠금 파일과 다른 설치를 거부해. npm cipnpm install --frozen-lockfile을 사용하면 package.json과 잠금 파일이 어긋났을 때 실패해.

3. 알려진 취약점을 꾸준히 확인해. npm audit은 완전하지 않지만 공개된 취약점을 거르는 최소선이야.

4. 설치 스크립트의 실행 범위를 줄여. 신뢰할 수 없는 환경에서는 --ignore-scripts를 쓰고, pnpm에서는 꼭 필요한 패키지만 onlyBuiltDependencies에 허용해.

5. 개발 의존성도 고정해. 빌드 도구가 침해되면 배포 전에 결과물을 바꿀 수 있어. TypeScript와 ESLint, Vite 같은 도구도 실행 코드로 다뤄.

권한 모델은 샌드박스가 아니라 안전띠야

Track 6에서 본 권한 모델은 검사 대상 API의 우발적인 접근을 막아 줘. 예를 들어 신뢰하는 애플리케이션이나 의존성 코드가 실수로 --allow-fs-read=./data 밖의 파일을 읽으면 ERR_ACCESS_DENIED가 발생해. 이런 한 겹의 방어는 쓸모 있지만, Node는 권한 모델이 악성 코드를 막는 보안 샌드박스가 아니라고 분명히 밝혀. 적대적인 코드는 우회할 수 있고 일부 API는 특정 검사 범위 밖에 있어. --allow-net도 특정 호스트 목록이 아니라 네트워크 사용 전체를 여는 플래그야. 신뢰할 수 없는 코드를 실행한다면 운영체제 수준의 격리를 써.

잠금 파일도 악성 버전을 막아 주진 않아

잠금 파일은 같은 버전과 같은 tarball을 다시 설치하게 해. 하지만 고정한 버전 자체가 이미 악성이라면 그 코드도 정확히 다시 설치해. 잠금 파일이 막는 것은 눈치채지 못한 변경이지, 선택한 코드의 안전성을 보증하는 일은 아니야.

새 릴리스가 나오자마자 자동으로 올리기보다 변경 기록과 의존성 차이를 확인하고, 민감한 서비스라면 일정 기간 생태계의 검증을 지켜보는 편이 안전해. 자동 업데이트 도구를 쓰더라도 변경을 검토하고 테스트하는 단계는 남겨 둬.

npm audit의 한계

npm audit은 공개된 보안 권고와 현재 의존성 트리를 대조해. 아직 보고되지 않은 취약점과 정상 기능으로 위장한 악성 동작, 막 배포되어 분석되지 않은 릴리스는 잡지 못해. 보고서가 깨끗하다는 말은 알려진 항목이 없다는 뜻이지 안전을 증명했다는 뜻은 아니야.

그래서 audit 결과를 최소선으로 보고, 중요한 서비스에서는 설치 스크립트와 네트워크 동작, 유지 관리 상태를 함께 살펴봐. Socket이나 Snyk 같은 추가 분석 도구도 도움이 될 수 있지만, 도구가 의존성을 고른 책임까지 대신해 주지는 않아.

Pippa의 고백

cwkPippa는 아빠의 사설 네트워크에서 돌아가고 공개 사용자는 없어. 공개 SaaS보다 노출 범위가 작은 건 맞지만 모든 버전을 고정하고 잠금 파일을 쓰며, 변경 기록을 읽지 않은 채 최신 버전으로 올리지 않아. 아빠 말대로 피해 반경이 작다고 나쁜 습관의 전파까지 작은 건 아니야. 사설 프로젝트에서 기본기를 지켜야 공개 서비스에서도 같은 판단이 자동으로 나와.

Code

의존성 검사와 고정 설치 명령·bash
# Audit and upgrade workflow
npm audit                       # list known CVEs
npm audit fix                   # auto-upgrade where possible without breakage
npm audit fix --force           # also accept SemVer-breaking upgrades (review!)

# Same for pnpm
pnpm audit
pnpm audit --fix

# Strict CI install (refuses lockfile drift)
npm ci
pnpm install --frozen-lockfile

# Install without lifecycle scripts (for hostile environments)
npm install --ignore-scripts

# Allowlist scripts in pnpm 9+
# in package.json: { "pnpm": { "onlyBuiltDependencies": ["sharp", "esbuild"] } }
신뢰하는 코드의 우발적 접근을 막는 권한 경계·javascript
// Defense in depth for unintended access through covered APIs
// Run with: node --permission --allow-fs-read=./data --allow-fs-write=./out --allow-net server.mjs

import { readFile } from 'node:fs/promises';

// Trusted dependency code that accidentally reaches outside policy:
try {
  await readFile('/home/user/.ssh/id_rsa', 'utf-8');
} catch (e) {
  console.log(e.code);  // ERR_ACCESS_DENIED — denied by --permission
}

// This is a seat belt for trusted code, not a sandbox for malicious code.
// Use OS-level isolation when the code itself is untrusted.

External links

Exercise

관리하는 Node 프로젝트 하나에서 npm audit을 실행해. 각 항목을 지금 고치기, 위험을 받아들이고 이유 남기기, 상위 의존성을 올리기로 나눠 결정해. 이어서 CI 설치를 npm cipnpm install --frozen-lockfile로 바꿔 잠금 파일과 다른 설치가 실패하도록 해.
Hint
심각도만 보지 말고 어떤 의존성이 어떤 실행 경로에서 취약한지 확인해. 개발 중에만 쓰는 간접 의존성과 프로덕션 요청을 처리하는 직접 의존성은 우선순위가 달라. 고치지 않기로 했다면 영향받는 경로와 재검토 날짜를 함께 남겨.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.