NPM Audit으로 패키지 취약점 관리하기

bagelcode

Introduction

안녕하세요. 베이글코드 Platform팀 Server Engineer 이종현입니다.

오늘은 수많은 npm 패키지들의 보안 취약점을 검사해 주는 npm audit에 대한 주제로 이야기해보려고 합니다. npm audit은 CLI 커맨드이며, NSP 팀이 npm에 합류하면서 npm v6에 기능이 추가되었습니다. NSP 팀은 npm의 보안과 관련하여 오랫동안 분석을 해오며 nsp라는 tool을 만들기도 한 팀인데요, npm이 그러한 NSP의 데이터베이스에서 취약점에 대한 정보를 이용할 수 있게 된 것입니다.

npm audit에 대한 기본적인 내용과 CI/CD에서의 활용, 그리고 취약점을 직접 고치며 겪은 이슈까지 다뤄볼 예정입니다.

Version

팀에서 사용 중인 Node.js와 npm 버전으로 작업을 진행했기 때문에 아래 버전을 기준으로 작성하였습니다.

Node.js: v14
npm: v6

Audit Report?

우선 npm audit을 입력하면 아래와 같은 report가 나오게 됩니다. Report 포맷은 npm 버전별로 차이가 있지만 기본적으로 Severity, Description, Package, Dependency of, Path 등을 볼 수 있습니다. 각각의 의미에 대해 살펴보겠습니다.

Audit Security Report

Severity

취약성의 심각도를 나타냅니다. 취약점의 영향과 악용 가능성에 따라 결정됩니다. 기본적으로 취약점은 CVSS (Common Vulnerability Scoring System)에 의해 Score 와 심각도를 부여받는데, 그 심각도와 일치하는 것으로 보였습니다.

Severity

Description

취약점에 대한 설명입니다. 예를 들어  Denial of Service 나  SSRF  등으로 나옵니다.

Package

취약점이 발견된 패키지의 이름입니다.

Patched in

해당 취약점이 보완된 semantic version의 range입니다. 여기에 맞는 버전을 설치하면 취약점을 해소할 수 있습니다.

Dependency of

취약점이 발견된 패키지에 의존하는 패키지입니다.

Path

취약점이 있는 패키지까지의 경로를 나타냅니다.

More info

Security report의 링크가 나오고, 취약점에 관한 더 자세한 정보를 볼 수 있습니다.

Report 대응방법

Report를 보고 취약점을 확인했다면 대응을 해줘야 하는데요, 아래는 npm 공식 문서에서 제시하고 있는 매뉴얼입니다. 총 3가지 정도의 케이스로 나누어 볼 수 있습니다.

Suggested Update

Report를 보면 취약점을 해결하기 위해 특정 버전을 설치하라고 가이드 해주는 경우가 있습니다. 아래와 같이  class-validator  0.14.0을 설치하면 취약점을 제거할 수 있습니다.

이러한 케이스들에 대해서는 npm audit fix로 한 번에 업데이트할 수도 있습니다.

Suggested Update

Semver Warning

두 번째 케이스는 Semver Warning입니다. 위와 같이 버전 가이드를 해주지만, 경우에 따라 Semver Warning이 같이 나오는 경우가 있습니다. 이 경우는 현재 사용 중인 패키지와 major 버전이 다른 경우에 띄워주는 경고입니다.

Major 버전 업그레이드는 패키지의 interface에 큰 변화가 있을 확률이 높기 때문에, 취약점의 severity나 내용에 따라 업그레이드를 해야 할지 판단이 필요합니다.

이 경우에는 npm audit fix 로 업데이트되지 않고, npm audit fix --force 와 같이 force 옵션을 주면 한 번에 업데이트가 가능합니다.

Semver Warninig

Manual Review

마지막 케이스는 취약점이 보완된 버전이 나오지 않은 경우로,  Manual Review 로 따로 나오게 됩니다. 단순 버전 업데이트로는 해결되지 않기 때문에, 별도의 대응이 필요합니다. 이 경우 npm에서는 3가지 정도의 방법을 제시하고 있습니다.

Manual Review

제한적 사용

해당 취약점이 모든 환경에서 취약하지 않을 수 있습니다. 예를 들어 특정 os에서 실행되거나, 특정 함수가 호출되었을 때만 취약할 수 있기 때문에, 그렇지 않은 경우에는 사실상 사용해도 됩니다. 또한 패키지의 사용자는 결국 우리 개발자들이기 때문에, 직접 소스코드를 수정하거나 머신까지 들어와야 생길만한 취약점은 신경 쓰지 않아도 됩니다.

의존하는 패키지에 PR(MR) 요청

취약점이 있는 패키지가 패치 버전이 나왔지만, 사용하는 패키지가 업데이트를 하지 않고 있을 수도 있습니다. 예를 들어 위 Manual Review Report에서  dicer 에 취약점이 개선된 버전이 나와도  busboy 에서 업데이트를 하지 않으면 무용지물입니다. 이런 경우  busboy 에게  dicer 를 업데이트 해달라는 PR(MR)을 요청하여 해결할 수 있습니다.

해당 패키지에 직접 PR(MR) 요청

취약점이 개선된 버전이 아예 나오지 않았다면 취약한 패키지에 직접 PR(MR)을 요청하거나, 이슈를 열 수도 있습니다.

CI/CD에 포함시키기

CI/CD에서 특정 severity 이상의 취약점이 존재하는 경우 빌드를 실패하게 하고 싶다면 다음 커맨드를 이용하면 됩니다.

npm audit --audit-level=critical

--audit-level : 해당 level 이상인 경우 0이 아닌 code로 exit되어 빌드가 실패합니다. 하지만 stdout으로는 모든 level의 report를 보여주기 때문에 확인은 할 수 있습니다.

Dockerfile 내에 포함시킨다면 아래와 같이 사용 가능합니다.

# Dockerfile
FROM node:14
# choose working directory
WORKDIR /usr/src/app
# copy all project
COPY . .
# check npm package vulnerabilities
RUN npm audit --audit-level=critical
# install dependencies
RUN npm ci
# build
RUN npm run build

직접 적용 해보기

위 매뉴얼을 기반으로 저희 팀에서도 vulnerability를 제거해 보았습니다. 대부분의 경우 Manual Review로 나오지 않는다면 버전 업데이트로 해결할 수 있었습니다. 그러나 결제 서버에서 사용 중인  in-app-purchase  패키지에서 업데이트로 해결되지 않는 경우가 있었습니다.

Report를 보면  in-app-purchase 가 의존하는 몇몇 패키지들( xml-cryptoxmldomrequest )에서 취약점이 발견된 것을 확인할 수 있었습니다.

그래서  in-app-purchase 에서 관련하여 조치를 취해줄 가능성이 있는지 먼저 살펴보았습니다. Snyk나 여러 개발자들이 남겨놓은 PR 및 이슈들을 확인할 수 있었지만 merge도 되지 않고 있었고, 마지막 버전이 4년 전에 개발되어 앞으로 새로운 버전이 나올 가능성은 낮아 보였습니다.

Open 상태
4 years ago

직접 수정하기로 결정

결론적으로 당장 해결할 방법이 보이지 않았고, 직접 수정하여 사용하기로 결정하였습니다. 버전 관리는 사내 코드 저장소에 fork를 해와서 관리하고, 패키지 업로드 및 설치는 사내 private registry를 사용하였습니다.

패키지 수정

사내 코드 저장소로 옮겨왔다면, 이제 원하는 대로 수정하면 됩니다. 기존 report에서 문제가 되었던 패키지들을 업그레이드하였습니다. 참고로  xmldom 은  @xmldom/xmldom 으로 이름이 변경되었습니다.

package.json

그 후에 npm audit을 통해 다시 report를 받아 봅니다.

그러나 기대했던 바와 달리,  request 는 취약점이 제거된 버전이 나오지 않아 직접 수정해도 해결되지 않았습니다. 이를 해결하기 위한 방법은 두 가지 정도로 생각해 볼 수 있는데요.

  • request  패키지에 PR 요청
  • 대체 패키지 사용

하지만  request  패키지가 deprecated 되었고, 추가 개발을 멈추어 첫 번째 방법은 불가능했습니다. 이와 관련된 자세한 내용은 여기에 더 나와있습니다.

Deprecated!

대체 패키지를 사용하려면 코드 수정이 필요하기도 하고, 이 부분은 TODO 리스트로 남겨둔 상태입니다. 이처럼 dependency로 엮여 있는 경우에는 해결하기가 쉽지 않습니다.

지금까지 패키지 취약점을 해결하기 위해 겪은 과정과 이슈들을 정리해 보았습니다.

꼭 해소해야 하는가?

마지막으로 취약점을 꼭 해소해야 하는가?에 대해 생각해 보고 마무리하겠습니다.

Manual Review의 대응 매뉴얼에서도 나와있듯이, 상황에 따라 report에 취약점이 존재하더라도 사용할 수 있습니다. 다만 이를 위해서는 취약점과 패키지에서 언제 취약한지에 대한 분석이 필요합니다.

SSRF

위  request  패키지에서 발견된 취약점인 SSRF은 악의적인 url을 이용해 server 리소스에 접근하는 공격입니다.  request 에서 발현되려면 input url이 악의적이어야 하는데, 이 값은  in-app-purchase 에서 관리하고 있습니다. 따라서  request 의 input url이 변경되기는 쉽지 않아 보입니다.

이러한 취약점에 대한 분석은 audit report의 More Info의 도움을 받을 수 있습니다.

log4j

반면 2021.12.09에 보고된 log4j의 임의 코드 실행 취약점에 대해 생각해본다면, 패키지의 취약점을 간과할 수 없는 것도 사실입니다. 실제로 이 취약점은 NVD라는 기관에게서 가장 높은 10점의 Score를 받았습니다.

패키지가 잘 관리되고 있다면 패치가 주기적으로 되기 때문에 버전 업데이트 만으로 해결이 가능합니다. 다만 Manual Review로 나오는 경우에는 그 취약점에 대한 분석을 한 뒤에 의사결정을 하는 것이 바람직하다고 생각합니다.

마무리

이번 포스트에서는 npm audit에 대한 설명, CI/CD에 취약점 검사를 포함시키는 법, 실제로 vulnerability를 해결하는 과정에서 겪은 이슈들까지 다루어 보았습니다.

in-app-purchase 의 예시에서는 패키지를 직접 수정하는 방식을 택했지만, 버전 업데이트로 해결이 안 된다고 해서 항상 이런 방식을 택하는 것은 현실적이지 않습니다. 사실 해당 케이스의 경우 취약점뿐만 아니라 다른 부분도 사내에서 관리가 필요하다고 판단되어 fork를 한 것이기도 합니다. 그렇기 때문에 선택할 수 있는 하나의 방법 정도로 생각해 주시면 될 것 같습니다. 😄