오픈소스 라이브러리 취약점 점검: npm audit 이후 8가지
오픈소스 라이브러리 취약점 점검을 경고 개수 줄이기가 아니라 위험 판단으로 바꾸는 법. 도달 가능성 판단, lockfile 관리, 버려진·타이포스쿼팅 패키지 확인, 컨테이너 이미지 점검까지 8가지 체크리스트로 정리했습니다.
npm audit에 경고가 수십 개 쌓여 있다면 위에서부터 차례로 고치기보다 운영 코드에서 실행되는 패키지인지, 그 취약한 함수에 외부 입력이 닿는지부터 확인하세요. 프로덕션 의존성이면서 사용자 입력이 닿는 경로에 있는 취약점이 가장 먼저 고칠 대상입니다. 빌드 도구나 테스트 도구에만 있는 경고는 일정을 잡아서 처리하면 됩니다.
다만 npm audit은 "알려진 CVE가 있는 버전"만 찾아 줍니다. 최근 공급망 사고는 대부분 그 밖에서 일어났습니다. 탈취된 계정으로 배포된 악성 버전, 이름이 비슷한 가짜 패키지, 몇 년째 관리되지 않는 패키지, 오래된 컨테이너 베이스 이미지 같은 것들입니다. OWASP Top 10 2025가 기존의 "취약하고 오래된 구성요소" 항목을 A03 Software Supply Chain Failures로 넓혀 빌드 시스템과 배포 경로까지 포함한 것도 같은 이유입니다.
이 글에서는 경고를 0개로 만드는 대신 실제 위험을 줄이는 것을 목표로, 출시 전과 운영 중에 확인할 8가지를 정리했습니다.
1. 경고를 '실행 경로' 기준으로 다시 분류한다
심각도(Critical/High)만 보고 순서를 정하면 테스트 러너의 정규식 취약점을 붙잡고 있는 사이에 API 서버가 쓰는 파서 취약점이 뒤로 밀립니다. 먼저 두 가지 축으로 나눠 보세요.
# 운영 번들/서버에 실제로 들어가는 의존성만 보기
npm audit --omit=dev
# 이 패키지가 왜 설치됐는지(누가 끌고 왔는지) 확인
npm explain vulnerable-parser
npm ls vulnerable-parser
| 구분 | 직접 의존성 | 전이 의존성 |
|---|---|---|
| 프로덕션 | 최우선. 바로 업데이트를 검토 | 상위 패키지 업데이트 또는 overrides 검토 |
| 개발 전용 | 다음 정기 업데이트 때 처리 | 보통 가장 낮음. 다만 CI에서 실행되면 예외 |
개발 의존성이라고 무조건 안전한 것은 아닙니다. CI 러너에는 배포 키와 레지스트리 토큰이 들어 있는 경우가 많아서, 빌드 단계에서 악성 코드가 실행되면 운영 코드에 들어가지 않았어도 피해가 생깁니다. "알려진 취약점"은 실행 경로로 판단하고, "악성 패키지"는 설치되는 순간부터 위험하다고 보는 식으로 기준을 따로 두는 게 좋습니다.
GitHub을 쓴다면 Dependabot auto-triage 규칙으로 개발 범위 의존성의 영향이 낮은 경고를 자동으로 닫을 수 있습니다. 자동으로 닫힌 경고도 기록에 남고, 의존성이 프로덕션 범위로 바뀌는 등 메타데이터가 달라지면 다시 열릴 수 있다는 점까지 알고 쓰세요.
2. 취약한 코드에 실제로 도달하는지 판단한다
권고문(advisory)에는 보통 어떤 함수가 어떤 입력을 받을 때 문제가 생기는지 적혀 있습니다. 이걸 우리 코드와 맞춰 보는 것이 도달 가능성(reachability) 판단입니다. 예를 들어 가상의 deep-merge-lite 패키지의 merge()에 프로토타입 오염 취약점이 보고됐다고 해 보겠습니다.
// 위험: 요청 본문이 그대로 취약 함수에 들어간다 → 도달 가능
import { merge } from 'deep-merge-lite';
app.patch('/api/settings', (req, res) => {
const settings = merge(loadSettings(req.user.id), req.body);
saveSettings(req.user.id, settings);
res.json(settings);
});
// 개선: 업데이트와 별개로, 허용한 필드만 취약 함수에 넘긴다
const ALLOWED = ['theme', 'language', 'notifications'];
app.patch('/api/settings', (req, res) => {
const patch = {};
for (const key of ALLOWED) {
if (Object.hasOwn(req.body, key)) patch[key] = req.body[key];
}
const settings = merge(loadSettings(req.user.id), patch);
saveSettings(req.user.id, settings);
res.json(settings);
});
같은 패키지를 빌드 스크립트에서 저장소 안의 고정 설정 파일끼리 합치는 데만 쓴다면, 외부 입력이 닿지 않으니 당장의 위험은 훨씬 낮습니다. 판단할 때는 아래를 확인하세요.
- 취약한 함수나 옵션을 우리 코드(또는 우리가 쓰는 상위 라이브러리)가 호출하는가
- 그 호출에 사용자 입력, 업로드 파일, 외부 URL 응답 같은 신뢰할 수 없는 데이터가 들어가는가
- 인증 전에 도달할 수 있는 경로인가, 관리자만 쓰는 경로인가
- 영향이 서비스 거부인지, 원격 코드 실행이나 데이터 노출인지
"도달하지 않음"으로 판단했다면 근거를 이슈에 남기세요. 코드가 바뀌면 판단도 달라지기 때문입니다. 위험도와 조치 우선순위를 읽는 방법은 취약점 점검 리포트 읽는 법에서도 같은 관점으로 다뤘습니다.
3. 고칠 때는 업데이트 → overrides → 교체 순으로
npm audit fix --force는 메이저 버전 업데이트까지 적용할 수 있어서, 보안 패치를 하다가 기능 장애를 내는 흔한 원인이 됩니다. 아래 순서를 권합니다.
- 직접 의존성 업데이트: 패치/마이너 범위에서 해결되면 테스트 후 반영합니다.
- 상위 패키지 업데이트: 전이 의존성이라면 그것을 끌고 오는 상위 패키지의 새 버전이 이미 고쳤는지 먼저 봅니다.
- overrides로 고정: 상위 패키지가 대응하지 않을 때만 씁니다.
{
"overrides": {
"vulnerable-parser": "2.4.1"
}
}
overrides는 상위 패키지가 테스트하지 않은 조합을 강제로 만드는 것이니, 왜 넣었는지 주석 대신 이슈 링크를 남기고 상위 패키지가 고쳐지면 지우세요. 쌓아두기만 하면 그 자체가 또 하나의 기술 부채가 됩니다.
- 교체 또는 제거: 패치가 나올 기미가 없고 도달 가능하다면 대체 라이브러리를 쓰거나 표준 API로 바꾸는 편이 깔끔한 경우가 많습니다.
4~5. lockfile과 설치 단계를 잠근다
4. lockfile은 코드처럼 리뷰한다
package-lock.json은 실제로 설치되는 정확한 버전과 출처를 기록합니다. 이 파일을 커밋하지 않거나 CI에서 npm install을 쓰면 빌드할 때마다 다른 버전이 들어올 수 있습니다. npm ci는 lockfile이 반드시 있어야 하고, package.json과 맞지 않으면 lockfile을 고치지 않고 실패합니다. CI와 배포에는 이쪽이 맞습니다.
- lockfile 변경이 포함된 PR은 어떤 패키지가 새로 추가됐는지 diff로 확인한다
resolved주소가 사내 레지스트리나 공식 레지스트리가 아닌 곳을 가리키지 않는지 본다- 기능 변경 PR에 이유를 알 수 없는 대량의 lockfile 변경이 섞이지 않게 의존성 업데이트 PR을 따로 분리한다
5. 설치 스크립트와 '갓 나온 버전'을 경계한다
postinstall 같은 라이프사이클 스크립트는 설치하는 순간 실행됩니다. 2025년 9월 npm 생태계를 휩쓴 자기 복제형 웜 Shai-Hulud처럼, 탈취된 계정으로 배포된 악성 버전은 설치 단계에서 토큰을 빼가는 방식을 흔히 씁니다. CI에서는 스크립트를 기본으로 끄고, 꼭 필요한 패키지만 다시 빌드하는 구성을 권합니다.
# .github/workflows/ci.yml 일부
- run: npm ci --ignore-scripts
- run: npm rebuild esbuild # 네이티브 빌드가 필요한 패키지만 명시
- run: npm audit signatures # 레지스트리 서명·provenance 검증
- run: npm audit --omit=dev --audit-level=high
npm audit signatures는 설치된 패키지의 레지스트리 서명과 provenance(어느 저장소·빌드에서 나왔는지) 증명을 확인합니다. 다만 npm 공식 문서도 밝히고 있듯이 provenance가 있다고 해서 악성 코드가 없다는 보장은 아닙니다. 출처를 추적할 수 있다는 뜻일 뿐입니다.
pnpm을 쓴다면 minimumReleaseAge로 배포된 지 일정 시간이 지나지 않은 버전은 설치하지 않게 할 수 있습니다. 악성 버전은 발견되어 내려가기까지 시간이 걸리는 경우가 많아서, 하루 정도 지연만 둬도 노출 위험이 줄어듭니다. 대신 긴급 보안 패치는 예외 목록(minimumReleaseAgeExclude)으로 열어 주는 절차가 필요합니다.
# pnpm-workspace.yaml
minimumReleaseAge: 1440 # 분 단위, 24시간
minimumReleaseAgeExclude:
- vulnerable-parser # 긴급 패치 적용 시에만 한시적으로 추가
6~7. 버려진 패키지와 이름이 수상한 패키지를 걸러낸다
6. 관리가 끊긴 패키지를 찾는다
버려진 패키지에는 취약점이 보고돼도 패치가 나오지 않습니다. 또 관리자 계정이 방치되면 탈취당하거나 이름이 다른 사람에게 넘어갈 위험도 커집니다. 핵심 의존성(인증, 파싱, 파일 처리, 암호화)부터 확인하세요.
npm view some-package time.modified maintainers repository deprecated
npm outdated
- 설치할 때 나오는
deprecated경고를 무시하지 않는다 - 저장소가 보관(archived) 처리됐는지, 최근 이슈에 응답이 있는지 본다
- OpenSSF Scorecard의 Maintained 항목은 최근 90일 동안의 커밋과 이슈 활동을 봅니다. 만든 지 90일이 안 된 프로젝트는 판단하지 않으니 이런 경우는 직접 살펴봐야 합니다
대체재가 없으면 포크해서 직접 유지할지, 기능을 직접 구현할지를 결정하고 그 결정을 기록해 두세요.
7. 타이포스쿼팅과 의존성 혼동을 확인한다
expres, reqeusts처럼 유명 패키지와 한두 글자 다른 이름이나, 사내 패키지와 같은 이름의 공개 패키지가 섞여 들어오는 경우가 있습니다.
- 새 의존성을 추가할 때 공식 문서나 저장소에 적힌 설치 명령의 철자를 그대로 쓴다
- 다운로드 수가 적은데 이름은 유명 패키지와 거의 같다면, 저장소 링크와 관리자를 확인한다
- 사내 패키지는
@company/같은 스코프를 쓰고.npmrc에서 그 스코프를 사내 레지스트리로 고정한다
# .npmrc
@company:registry=https://npm.internal.example.com/
AI 코딩 도구가 추천한 패키지 이름도 존재 여부와 실제 저장소를 꼭 확인한 뒤 설치하세요.
8. 컨테이너 베이스 이미지까지 점검 범위에 넣는다
npm audit이 깨끗해도 배포 이미지 안의 OS 패키지(openssl, glibc 등)는 따로 관리해야 합니다. 흔한 문제는 태그가 고정되지 않은 이미지, 빌드 도구가 그대로 남은 최종 이미지, 몇 달째 다시 빌드하지 않은 이미지입니다.
# 개선 전
FROM node:latest
COPY . .
RUN npm install
CMD ["node", "server.js"]
# 개선 후: 버전 고정 + 멀티 스테이지 + 운영 의존성만 + 비루트 실행
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
그다음 이미지 스캐너(Trivy, Grype 등)로 OS 패키지와 언어 패키지를 함께 검사합니다. 코드를 바꾸지 않았어도 베이스 이미지 보안 업데이트를 받으려면 정기적으로 다시 빌드해야 한다는 점을 운영 일정에 넣어 두세요. 재현성이 중요하다면 태그 대신 digest(@sha256:...)로 고정하고, 그 digest를 Dependabot이나 Renovate로 갱신하는 방식이 관리하기 편합니다.
출시 전·운영 중 체크리스트
출시 전
-
npm audit --omit=dev결과의 High 이상을 도달 가능성 기준으로 판단하고 근거를 기록했다 - lockfile이 커밋돼 있고, CI·배포는
npm ci를 쓴다 - CI에서 설치 스크립트를 기본으로 끄고, 예외 패키지를 명시했다
- 핵심 의존성 중 deprecated이거나 보관 처리된 패키지가 없다
- 사내 패키지는 스코프와 레지스트리가 고정돼 있다
- 배포 이미지는 버전을 고정했고, 이미지 스캔을 통과했다
운영 중
- 의존성 업데이트 PR을 주 단위로 묶어서 검토한다
- 베이스 이미지를 정기적으로 다시 빌드한다
- npm 배포 권한이 있다면 장기 토큰 대신 trusted publishing을 쓰고, 2FA를 켠다
- 악성 패키지 공지가 나오면 lockfile에서 해당 버전이 있는지 확인하고, CI와 개발 PC의 토큰을 교체하는 절차를 준비해 둔다
마지막 항목의 토큰 교체 범위는 API 키 유출 방지 체크리스트를 참고해 미리 목록으로 만들어 두면 사고가 났을 때 훨씬 빨리 움직일 수 있습니다.
자주 묻는 질문
npm audit 경고를 전부 0으로 만들어야 하나요?
아닙니다. 목표는 도달 가능한 취약점을 없애는 것입니다. 도달하지 않는 개발 의존성 경고는 근거를 남기고 정기 업데이트 때 처리해도 됩니다. 억지로 --force를 쓰다가 장애가 나는 쪽이 더 큰 비용이 되는 경우가 많습니다.
Dependabot PR을 자동으로 병합해도 되나요?
테스트가 충분하다면 개발 의존성의 패치 버전 정도는 자동 병합을 고려해 볼 만합니다. 하지만 프로덕션 의존성이나 메이저 업데이트는 사람이 검토하는 게 좋고, 배포된 지 얼마 안 된 버전을 바로 받지 않도록 지연 설정을 함께 두는 것을 권합니다.
컨테이너 이미지 스캔에서 '수정 버전 없음' 취약점이 많이 나옵니다.
수정 버전이 없는 항목은 당장 조치할 수 없으니, 수정 가능한 항목과 분리해서 보세요. 슬림하거나 최소화된 베이스 이미지로 바꾸면 쓰지 않는 패키지 자체가 줄어서 이런 경고도 같이 줄어드는 경우가 많습니다.
정리
- 경고는 심각도보다 "프로덕션에서 실행되는가, 외부 입력이 닿는가"를 기준으로 우선순위를 정합니다.
- lockfile과
npm ci, 설치 스크립트 차단, 신규 버전 지연으로 알려진 CVE 밖의 공급망 위험을 줄입니다. - 버려진 패키지, 이름이 수상한 패키지, 오래된 베이스 이미지까지 정기 점검 범위에 넣습니다.
- 우리 서비스의 의존성 구성을 직접 점검하기 어렵다면 무료 분석 신청으로 현재 상태를 진단받아 볼 수 있습니다.
참고 자료
- OWASP Top 10 2025 - A03 Software Supply Chain Failures
- npm Docs - Viewing package provenance
- npm Docs - Generating provenance statements
- npm Docs - npm audit (v10)
- npm CLI docs - npm ci
- pnpm Settings - minimumReleaseAge
- GitHub Docs - Dependabot auto-triage rules
- OpenSSF Scorecard - checks.yaml (Maintained)
- GitHub Changelog - Strengthening npm security: authentication and token management
- GitHub Blog - Our plan for a more secure npm supply chain
함께 읽으면 좋은 글
모바일 앱 API 보안: 디컴파일을 전제로 한 서버 설계
모바일 앱 API 보안을 '앱은 결국 분석된다'는 전제에서 정리했습니다. 프록시로 드러나는 정보, 피닝·난독화의 한계, 가격·등급 서버 검증, 토큰 저장 위치, 강제 업데이트 설계와 점검표를 담았습니다.
Mass Assignment 취약점: 회원정보 수정으로 권한 상승되는 원리
Mass Assignment 취약점이 회원정보 수정 API에서 role·isAdmin 변경으로 이어지는 원리와, Express·Prisma·Spring·Django 코드에서 허용 필드 화이트리스트로 고치는 방법을 비교 코드와 체크리스트로 정리했습니다.
개인정보 파기 방법: 회원 탈퇴 후 DB·백업·로그 처리법
개인정보 파기 시점과 방법, 법정 보존 데이터의 분리 보관, 백업·로그·외부 서비스 사본 처리, 파기 기록까지 회원 탈퇴 흐름을 구현 단위로 정리했습니다.