(번역) NPM 보안 모범 사례

sehyun hwang·2025년 10월 27일
6

FE 번역글

목록 보기
45/46
post-thumbnail

원문 : https://github.com/bodadotsh/npm-security-best-practices

📝 Note
NPM 생태계는 손상^1,^2, 공급망 공격^3, 악성코드^4,^5, 스팸^6, 피싱^7, 사고^8 또는 심지어 트롤^9에 익숙합니다. 이 글에서는 이러한 상황들로부터 보호하는 데 유용할 수 있는 정보를 정리했습니다.

관련하여 부담없이 Pull Request를 제출하거나, Twitter로 연락 주세요!

💡 Tip
이 저장소는 npm, bun, deno, pnpm, yarn 등을 다룹니다.

목차

개발자를 위한 지침

💡 Tip
아래에 언급된 구성이 포함된 샘플 .npmrc 파일이 있습니다:

ignore-scripts=true
provenance=true
save-exact=true
save-prefix=''

다른 구성 파일 예시는 다음과 같습니다:

1. 종속성 버전 고정

npm에서 새로운 종속성은 기본적으로 Caret ^ 연산자와 함께 설치됩니다. 이 연산자는 가장 최근의 minor 또는 patch 릴리스를 설치합니다. 예를 들어, ^1.2.31.2.3, 1.6.2 등을 설치합니다. https://docs.npmjs.com/about-semantic-versioning를 참고하여 npm SemVer Calculator (https://semver.npmjs.com/)를 시도해 보세요.

다양한 패키지 관리자에서 정확한 버전을 고정하는 방법은 다음과 같습니다:

npm install --save-exact react
pnpm add --save-exact react
yarn add --save-exact react
bun add --exact react
deno add npm:react@19.1.1

구성 파일(예: .npmrc)에서 save-exact 또는 save-prefix 키와 값 쌍으로 이 설정을 업데이트할 수도 있습니다:

npm config set save-exact=true
pnpm config set save-exact true
yarn config set defaultSemverRangePrefix ""

bun의 경우 구성 파일은 bunfig.toml이며 다음과 같이 설정할 수 있습니다:

[install]
exact = true

간접(transitive) 종속성 재정의

그러나, 직접 종속성도 자체적인 종속성(간접 종속성)을 가지고 있습니다. 직접 종속성을 고정하더라도, 그들의 간접 종속성은 여전히 넓은 버전 범위 연산자(^ 또는 ~)를 사용할 수 있습니다. 해결책은 간접 종속성을 재정의하는 것입니다: https://docs.npmjs.com/cli/v11/configuring-npm/package-json#overrides

package.json에 다음 overrides 필드가 있는 경우:

{
  "dependencies": {
    "library-a": "^3.0.0"
  },
  "overrides": {
    "lodash": "4.17.21"
  }
}
  • library-apackage.json"lodash": "^4.17.0" 종속성을 가지고 있다고 가정합니다.
  • overrides 섹션이 없으면, npmlodash@4.17.22(또는 최신 4.x.x 버전 중 하나)를 library-a의 간접 종속성으로 설치할 수 있습니다.
  • 그러나 "overrides": { "lodash": "4.17.21" }을 추가하면, 종속성 트리에서 lodash가 나타날 때마다 정확히 버전 4.17.21로 해석되도록 npm에 지시합니다.

pnpm의 경우 overrides 필드를 pnpm-workspace.yaml 파일에 정의할 수도 있습니다: https://pnpm.io/settings#overrides

yarn의 경우 overrides 이전에 resolutions 필드를 도입하였으며 유사한 기능을 제공합니다: https://yarnpkg.com/configuration/manifest#resolutions

{
  "resolutions": {
    "lodash": "4.17.21"
  }
}
# yarn은 resolution을 설정하는 CLI도 제공합니다: https://yarnpkg.com/cli/set/resolution
yarn set resolution <descriptor> <resolution>

bun의 경우 overrides 필드 또는 resolutions 필드를 지원합니다: https://bun.com/docs/install/overrides

deno의 경우 자세한 내용은 denoland/deno#28664를 참고하세요.

2. Lockfile 포함

패키지 관리자 lockfile을 git에 커밋하고 서로 다른 환경 간에 공유하세요. lockfile의 예시로는 npmpackage-lock.json, pnpmpnpm-lock.yaml, bunbun.lock, yarnyarn.lock, denodeno.lock가 있습니다.

CI/CD 같은 자동화 환경에서는 lockfile에 정의된 정확한 종속성을 설치해야 합니다.

npm ci
bun install --frozen-lockfile
yarn install --frozen-lockfile
deno install --frozen

deno의 경우 deno.json 파일에 다음과 같이 설정할 수 있습니다:

{
  "lock": {
    "frozen": true
  }
}

💡 Tip
lockfile의 병합 충돌을 처리할 때 lockfile을 삭제할 필요가 없습니다. 종속성(간접 포함)이 버전 범위 연산자(^, ~ 등)로 정의된 경우, lockfile을 처음부터 다시 빌드하면 예상치 못한 업데이트가 발생할 수 있습니다.

최신 패키지 관리자는 충돌 해결 기능^18,^19을 내장하고 있으며, main을 체크아웃하고 install을 다시 실행하면 됩니다. pnpm은 또한 Git Branch Lockfiles를 허용하며, 브랜치 이름에 따라 새로운 lockfile을 생성하고 나중에 메인 lockfile로 자동 병합합니다.

3. 라이프사이클 스크립트 비활성화

라이프사이클 스크립트는 pre<event>, post<event>, <event> 스크립트 외에 발생하는 특수 스크립트입니다. 예를 들어, preinstallinstall이 실행되기 전에 실행되고 postinstallinstall이 실행된 후에 실행됩니다. npm이 "scripts" 필드를 처리하는 방법은 https://docs.npmjs.com/cli/v11/using-npm/scripts#life-cycle-scripts를 참조하세요.

라이프사이클 스크립트는 악성 행위자들이 흔히 사용하는 전략입니다. 예를 들어, "Shai-Hulud" 웜^3package.jsonpostinstall 스크립트를 추가해 자격 증명을 탈취했습니다.

npm config set ignore-scripts true --global
yarn config set enableScripts false

bun, deno, pnpm의 경우 기본적으로 라이프사이클 스크립트가 비활성화되어 있습니다.

📝 Note
bun의 경우 상위 500개 npm 패키지의 라이프사이클 스크립트는 기본적으로 허용됩니다.

💡 Tip
앞서 언급한 플래그는 다양하게 조합할 수 있습니다. 예를 들어, 다음 npm 명령은 lockfile에 정의된 프로덕션 종속성만 설치하고 라이프사이클 스크립트를 무시합니다:

npm ci --omit=dev --ignore-scripts

4. 최소 릴리스 기간 설정

새로 게시된 패키지 설치를 방지하기 위해 지연 시간을 설정할 수 있습니다. 이는 간접 종속성을 포함한 모든 종속성에 적용됩니다. 예를 들어, pnpm v10.16minimumReleaseAge 옵션을 도입했습니다: https://pnpm.io/settings#minimumreleaseage, 이는 새 버전이 게시된 뒤 최소 몇 분이 지나야 설치할 수 있는지 정의하는 옵션입니다. minimumReleaseAge1440으로 설정되면 pnpm은 24시간 미만 전에 게시된 버전을 설치하지 않습니다.

pnpm config set minimumReleaseAge <minutes>

# 최소 1일 전에 게시된 패키지만 설치
npm install --before="$(date -v -1d)"

yarn config set npmMinimalAgeGate <minutes>

pnpm의 경우 특정 패키지를 최소 릴리스 기간에서 제외하는 minimumReleaseAgeExclude 옵션도 있습니다.

npm의 경우 minimumReleaseAge 옵션과 minimumReleaseAgeExclude 옵션을 추가하는 제안이 있습니다.

yarn의 경우 v4.10.0부터 npmMinimalAgeGatenpmPreapprovedPackages 옵션이 구현되었습니다.

bun의 경우 여기에서 논의되고 있습니다: oven-sh/bun#22679

deno의 경우 초안이 제안되었습니다: denoland/deno#30752

유사한 기능을 제공하는 다른 도구 예시:

5. 권한 모델

nodejs의 최신 LTS 버전에서는 권한 모델을 사용하여 프로세스가 액세스할 수 있는 시스템 리소스나 해당 리소스로 수행할 수 있는 작업을 제어할 수 있습니다. 그러나, 이는 악의적인 코드가 존재할 때 보안을 보장하지 않습니다. 악의적인 코드는 여전히 권한 모델을 우회하여 제한을 무시하고 임의 코드를 실행할 수 있습니다.

Node.js 권한 모델에 대해 읽어보세요: https://nodejs.org/docs/latest/api/permissions.html

# 기본적으로 전체 액세스 허용
node index.js

# 모든 사용 가능한 권한에 대한 액세스 제한
node --permission index.js

# 특정 권한 활성화
node --permission --allow-fs-read=* --allow-fs-write=* index.js

# `npx`와 함께 권한 모델 사용
npx --node-options="--permission" <package-name>

Deno는 기본적으로 권한을 활성화합니다: https://docs.deno.com/runtime/fundamentals/security/

# 기본적으로 액세스 제한
deno run script.ts

# 특정 권한 활성화
deno run --allow-read script.ts

Bun의 경우 권한 모델이 현재 여기여기에서 논의되고 있습니다.

6. 외부 종속성 줄이기

npm은 패키지를 게시하는 장벽이 낮기 때문에 생태계가 빠르게 성장하여 현재 500만 개 이상의 패키지를 가진 가장 큰 패키지 레지스트리로 성장했습니다^11. 그러나 모든 패키지가 동일하게 만들어진 것은 아닙니다. 스스로 코드를 작성할 수 있음에도 불구하고 의존성으로 다운로드되는 작은 유틸리티 패키지가 있으며^8, "우린 코딩하는 방법을 잊었나?^12"라는 질문을 제기합니다.

nodejs, bun, deno 사이에서 개발자는 서드파티 라이브러리에 의존하는 대신 많은 최신 기능을 사용할 수 있습니다. 네이티브 모듈이 동일한 수준의 기능을 제공하지 않을 수 있지만, 가능한 경우 고려해야 합니다. 다음의 몇 가지 예시가 있습니다.

NPM 라이브러리내장 모듈
axios, node-fetch, got, 등네이티브 fetch API
jest, mocha, ava, 등node:test,node:assert, bun testdeno test
nodemon, chokidar, 등node --watch, bun --watchdeno --watch
dotenv, dotenv-expand, 등node --env-file, bun --env-filedeno --env-file
typescript, ts-node, 등node --experimental-strip-types^10, denobun에 네이티브
esbuild, rollup, 등bun builddeno bundle
prettier, eslint, 등deno lintdeno fmt

다음은 유용할 수 있는 몇 가지 리소스 목록입니다.

메인테이너를 위한 지침

7. 2FA 활성화

https://docs.npmjs.com/about-two-factor-authentication

이중 인증(2FA)은 npm 계정에 인증 계층을 추가합니다. 2FA는 기본적으로 필수가 아니지만, 활성화하는 것이 좋습니다.

# 인증 및 쓰기에 대해 2FA가 활성화되었는지 확인 (기본값)
npm profile enable-2fa auth-and-writes
자동화 수준패키지 게시 액세스
수동각 패키지 액세스를 Require 2FADisable Tokens로 설정
자동각 패키지 액세스를 Require two-factor authentication 또는 Single factor automation tokens 또는 Single factor granular access tokens로 설정

⭐️중요
시간 기반 일회용 비밀번호(TOTP)^15 대신 WebAuthn을 지원하는 보안 키를 구성하는 것이 권장됩니다.

8. 제한된 접근 권한의 토큰 생성

https://docs.npmjs.com/about-access-tokens#about-granular-access-tokens

액세스 토큰은 API나 npm CLI를 사용할 때 흔히 쓰이는 인증 방식입니다.

npm token create # 읽기 및 게시 토큰용
npm token create --read-only # 읽기 전용 토큰용
npm token create --cidr=[list] # CIDR 제한 읽기 및 게시 토큰용
npm token create --read-only --cidr=[list] # CIDR 제한 읽기 전용 토큰용

⭐️중요
레거시 토큰 대신 세분화된 액세스 토큰을 사용해야 합니다. 레거시 토큰은 범위 지정이 불가능하고 자동으로 만료되지 않기 때문에 위험합니다.

  • 토큰을 특정 패키지, 범위, 조직으로 제한
  • 토큰 만료 날짜 설정 (예: 매년)
  • IP 주소 범위(CIDR 표기법)에 따라 토큰 액세스 제한
  • 읽기 전용 또는 읽기 및 쓰기 액세스 선택
  • 여러 목적으로 동일한 토큰 사용 금지
  • 설명적인 토큰 이름

9. 출처 증명서 생성

https://docs.npmjs.com/generating-provenance-statements

출처 증명은 빌드 환경에서 패키지의 소스 코드와 빌드 지침에 대한 링크를 공개적으로 제공하여 설정됩니다. 개발자는 다운로드 전에 패키지가 어디서, 어떻게 빌드되었는지 확인할 수 있습니다.

게시 증명은 인증된 사용자가 패키지를 게시할 때 레지스트리에 의해 생성됩니다. npm 패키지가 출처와 함께 게시되면 Sigstore 공공 서버에 의해 서명되고 공개 투명성 원장에 기록되어 누구나 확인할 수 있습니다.

예를 들어 vue의 출처 증명 예시는 여기에서 확인할 수 있습니다: https://www.npmjs.com/package/vue#provenance

출처를 설정하려면 지원되는 CI/CD (예: GitHub Actions)를 사용하고 올바른 플래그로 게시하세요.

npm publish --provenance

npm publish 명령을 직접 호출하지 않고도 다음과 같이 설정할 수 있습니다.

  • CI/CD 환경에서 NPM_CONFIG_PROVENANCEtrue로 설정
  • .npmrc 파일에 provenance=true 추가
  • package.jsonpublishConfig 블록 추가
"publishConfig": {
  "provenance": true
}

재현 가능한 빌드에 관심 있는 분들은 OSS Rebuild (https://github.com/google/oss-rebuild)와 Supply-chain Levels for Software Artifacts (SLSA) 프레임워크 (https://slsa.dev)를 확인하세요.

신뢰할 수 있는 게시

OpenID Connect (OIDC) 인증을 사용할 때 npm 토큰 없이 패키지를 게시하고 자동으로 출처를 얻을 수 있습니다. 이는 신뢰할 수 있는 게시라고 하며 GitHub 발표는 여기를 참고하세요: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/https://docs.npmjs.com/trusted-publishers

⭐️중요
토큰 대신 신뢰할 수 있는 게시를 사용하는 것이 권장됩니다^17.

10. 게시된 파일 검토

npm 패키지에 포함되는 파일을 제한하면 공격 표면을 줄여 악성코드를 방지하고 민감한 데이터의 우발적 유출을 피할 수 있습니다.

package.jsonfiles 필드는 게시된 패키지에 포함되어야 할 파일을 지정하는 데 사용됩니다. 항상 포함되는 파일도 있으며, 자세한 내용은 https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files를 참조하세요.

{
  "name": "my-package",
  "version": "1.0.0",
  "main": "dist/index.js",
  "files": ["dist", "LICENSE", "README.md"]
}

💡 Tip
.npmignore 파일을 사용하여 게시될 패키지에서 특정 파일을 제외할 수도 있습니다. 최상위의 "files" 필드를 덮어쓰지는 않지만, 하위 디렉터리에서는 재정의합니다.

.npmignore 파일은 .gitignore처럼 작동합니다. .gitignore 파일이 있고 .npmignore가 없으면 .gitignore의 내용이 대신 사용됩니다.

npm pack --dry-run 또는 npm publish --dry-run을 실행하여 pack/publish시 포함될 내용을 미리 확인하세요.

> npm pack --dry-run
npm notice Tarball Contents
npm notice 1.1kB LICENSE
npm notice 1.9kB README.md
npm notice 108B index.js
npm notice 700B package.json
npm notice Tarball Details

deno.json에서는 publish.includepublish.exclude 필드를 사용하여 포함하거나 제외할 파일을 지정합니다.

{
  "publish": {
    "include": ["dist/", "README.md", "deno.json"],
    "exclude": ["**/*.test.*"]
  }
}

그 외 기타

11. NPM 조직

https://docs.npmjs.com/organizations

조직 수준의 모범 사례는 다음과 같습니다.

  • 조직 수준에서 Require 2FA 활성화
  • npm 조직 멤버 수 최소화
  • 동일한 조직에 여러 패키지 팀이 있는 경우 모든 패키지에 대한 developers 팀 권한을 READ로 설정
  • 각 패키지의 권한을 관리하기 위해 별도의 팀 생성

12. 대체 레지스트리

JSR은 최신 자바스크립트와 타입스크립트를 위한 새로운 패키지 레지스트리이며, 기존 npm과 호환됩니다.

📝 Note

모든 npm 패키지가 JSR에 있는 것은 아닙니다!

https://jsr.io를 방문하여 패키지가 사용 가능한지 확인하고 npm 제한 사항 문서를 읽어보세요.

deno add jsr:<package-name>
pnpm add jsr:<package-name> # pnpm 10.9+
yarn add jsr:<package-name> # yarn 4.9+
# npm, bun, 그리고 오래된 버전의 yarn 또는 pnpm
npx jsr add <package-name> # npx를 yarn dlx, pnpm dlx, 또는 bunx로 대체

프라이빗 레지스트리

프라이빗 패키지 레지스트리는 조직이 자체 종속성을 관리하는 좋은 방법이며, 공용 npm 레지스트리에 대한 프록시 역할을 할 수 있습니다. 조직은 프로젝트에서 사용되기 전에 보안 정책을 시행하고 패키지를 검증할 수 있습니다.

다음은 유용한 몇 가지 프라이빗 레지스트리입니다.

13. 감사, 모니터링 및 보안 도구

감사

많은 패키지 관리자가 의존성의 알려진 취약점을 스캔하고, 보고서를 표시하며, 해결 방법을 추천하는 감사(audit) 기능을 제공합니다.

npm audit # 종속성 감사
npm audit fix # 호환되는 업데이트 자동 설치
npm audit signatures # 종속성 서명 확인

pnpm audit
pnpm audit --fix

bun audit

yarn npm audit
yarn npm audit --recursive # 간접 종속성 감사

GitHub

https://github.com/security

GitHub는 npm 악성코드로부터 보호하는 데 도움이 될 수 있는 여러 서비스를 제공합니다:

  • Dependabot: 이 도구는 npm 패키지를 포함한 프로젝트의 종속성을 알려진 취약점에 대해 자동으로 스캔합니다.
  • Software Bill of Materials (SBOMs): GitHub는 저장소의 종속성 그래프에서 직접 SBOM을 내보낼 수 있습니다. SBOM은 모든 프로젝트 종속성(간접 종속성 포함)의 포괄적인 목록을 제공합니다.
  • Code Scanning: 코드 스캐닝은 손상된 npm 패키지를 통합함으로써 발생할 수 있는 잠재적 취약점이나 의심스러운 패턴을 식별하는 데 도움이 될 수 있습니다.

⚠️ 주의
NPM 또는 Github에서 취약점이나 문제를 발견하면 다음 링크를 사용하여 보고하세요:

Socket.dev

https://socket.dev

Socket.dev는 취약하고 악의적인 종속성으로부터 코드를 보호하는 보안 플랫폼입니다. GitHub App 풀 리퀘스트 스캔, CLI 도구, 웹 확장, VSCode 확장 등 다양한 도구를 제공합니다. AI 기반 대규모 악성코드 사냥, 2025년 1월 발표를 참고하세요.

Snyk

https://snyk.io

Snyk는 오픈 소스 종속성의 취약점을 수정하는 도구 모음을 제공하며, 이는 로컬 머신에서 취약점 스캔을 실행하는 CLI, 개발 환경에 임베드하는 IDE 통합, Snyk를 프로그래밍 방식으로 통합하는 API를 포함합니다. 예를 들어, 사용 전에 공용 npm 패키지를 테스트하거나 알려진 취약점에 대한 자동 PR 생성을 할 수 있습니다.

14. OSS 지원

메인테이너 번아웃은 오픈 소스 커뮤니티에서 심각한 문제입니다. 많은 인기 있는 npm 패키지는 자원 봉사자들이 여가 시간을 쪼개서 관리하고 있으며, 대개는 어떠한 보상도 받지 않습니다. 시간이 지나면서 이는 피로와 동기 부여 부족으로 이어지며, 결국 “도움이 되는 기여자”로 위장한 악성 행위자의 사회공학에 취약해져 악성 코드가 주입되는 상황이 벌어질 수 있습니다.

2018년 event-stream 패키지의 메인테이너가 악성 행위자에게 액세스를 제공하여 손상되었습니다^13. JavaScript 생태계 외의 또 다른 예는 2024년 XZ Utils 사건^14으로, 악성 행위자가 3년 넘게 신뢰를 쌓아 권한을 획득했습니다.

OSS 기부는 오픈 소스 개발을 위한 더 지속 가능한 모델을 만드는 데 도움이 됩니다. 재단은 많은 사람들이 의존하는 수백 개의 오픈 소스 프로젝트 뒤의 비즈니스, 마케팅, 법적, 기술 지원 및 직접 지원을 돕습니다^15.

자바스크립트 생태계에서 OpenJS Foundation (https://openjsf.org)은 JS Foundation과 Node.js Foundation의 합병으로 2019년에 설립되어 가장 중요한 JS 프로젝트를 지원합니다. 매일 사용하는 OSS를 후원할 수 있는 몇 가지 다른 플랫폼은 다음과 같습니다:

0개의 댓글