LHJ

I'm a FE developer.

의존성 모듈의 취약점 해결하기

23 Sep 2020 » node_module

의존성 모듈의 취약점 해결하기

1. 상황

npm package.json을 활용해 모듈(=라이브러리) 버전 관리를 하면서 작업을 하다보면, 깃 또는 메일로 의존성 모듈의 취약점을 알리는 메시지와 메일이 잔뜩 오는 상황을 겪을 때가 있다.
이는 해당 레포에서 사용하는 외부(의존성) 모듈에 문제가 있음을 알리는 경고이다.
이를 해결하는 방법에 대한 가이드이다.

참고로 이런 경고 메시지는 해당 레포의 default 브랜치를 기준으로 한다.
default 브랜치가 master라면 master 브랜치를 기준으로 한다.

2. 의존성 모듈이란?

의존성 모듈에 대해 먼저 짚고 넘어가자.
npm에서 관리되는 모듈은 독자적인 모듈은 거의 없다.
A 모듈이 B 모듈에 영향을 주고 B 모듈은 C 모듈에 영향을 준다.

이때 C 모듈은 B 모듈에 ‘의존’하고 있다 라고 말한다.
왜냐하면 B 모듈이 없으면 C 모듈은 제대로 작동할 수 없기 때문이다.

이런 의존성을 package.json을 통해 관리해주는 것이 npm의 역할이다.

어떤 곳에서는 모듈 버전을 관리한다하고 어떤 곳에서는 라이브러리 버전을 관리한다고 한다.
모듈과 라이브러리는 같은 뜻이라고 생각하면 된다.

3. 모듈 버전의 의미

모듈 버전의 의미에 대해 짚고 넘어가자.
package.json에 명시된 모듈(=라이브러리) 버전을 보면 보통 아래와 같이 명시된 것을 확인할 수 있다.

{
  "dependencies": {
    "react": "^16.12.0"
  }
}

^ 이 문자를 캐럿이라고 하는데, 이 캐럿이 의미하는 바는 무엇일까?
아래 프로젝트 특징에서도 설명하겠지만, 위 버전에 대해 설명하기 전에 우선 버전관리에 대해 생각해보자.
만약 프로젝트에서 사용하는 모듈(=라이브러리)의 버전을 엄격하게 제한한다면 어떤 일이 발생할까?

모듈(=라이브러리) 버전업을 하는데 꽤 힘들 수 있다.
사용하는 패키지를 전부 버전업해야하기 때문이다.
그렇게되면 해당 프로젝트는 특정 버전에 갖혀 버릴지도 모른다.

하지만 반대로 버전을 느슨하게 풀어 놓는 것도 문제가 될 수 있다.
호환성 문제가 발생할 수 있다.

이를 위해 유의적 버전(Sementic Version) 체계가 만들어졌다.
npm은 이 유의적 버전을 따르는 전제 하에 모듈 버전을 관리한다.

유의적 버전은 주(Major), 부(Minor), 수(Patch) 세 가지 숫자를 조합해서 버전을 관리한다.
위으 react 라이브러리는 버전은 16.12.0이며 16이 주버전, 12가 부버전, 0이 수버전이라는 뜻이다.

  • 주버전(Major Version) : 기존 버전과 호환되지 않게 변경한 경우
  • 부버전(Minor Version) : 기존 버전과 호환되면서 기능이 추가된 경우
  • 수버전(Patch Version) : 기존 버전과 호환되면서 버그를 수정한 경우

버전을 관리하기 위해 보통 다음과 같은 방법을 사용합니다.

  1. 특정 버전을 명시한다.

    {
         "library": "1.2.3"
    }
    
  2. 부등호로 버전 범위를 명시한다.

    {
        "library": ">1.2.3",
        "library2": ">=1.2.3",
        "library3": "<1.2.3",
        "library4": "<=1.2.3"
    }
    
  3. 틸드(~)와 캐럿(^)을 사용해 범위를 명시한다.

    {
           "library": "~1.2.3",
           "library2": "^1.2.3"
       }
    

틸드
틸드(~)는 마이너(부) 버전이 명시되어 있으면 패치(수) 버전을 변경한다.
예를 들어 ~1.2.3 표기는 1.2.3 부터 1.3.0 미만까지를 포함한다.
마이너(부) 버전이 없으면 마이너(부) 버전을 갱신한다.
~0 표기는 0.0.0 부터 1.0.0 미만까지를 포함한다.

캐럿
캐럿(^)은 정식버전에서 마이너(부) 버전과 패치(수) 버전을 변경한다.
예를 들어 ^1.2.3 표기는 1.2.3 부터 2.0.0 미만까지를 포함한다.
정식버전 미만인 0.x 버전은 패치(수) 버전만 갱신한다.
^0 표기는 0.0.0 부터 0.1.0 미만까지를 포함한다.

과거에 NPM은 틸드를 기본으로 사용했는데 지금은 캐럿을 사용한다.

그 이유는 다음과 같다.

보통 라이브러리 정식 릴리즈 전에는 모듈 버전이 수시로 변한다.
0.1에서 0.2로 마이너(부) 버전이 변하더라도 하위 호환성을 지키지 않고 배포하는 경우가 빈번하다.
~0으로 버전 범위를 표기한다면 0.0.0 부터 1.0.0 미만까지 사용하기 때문에 하위 호환성을 지키지 못하는 0.2로 업데이트 되어버리는 문제가 발생할 수도 있다.

반면 캐럿을 사용해 ^0으로 표기한다면 0.0.0 부터 0.1.0 미만 내에서만 버전을 사용하도록 제한하기 때문에 하위 호환성을 유지할 수 있다.

4. 프로젝트 특징

보통 프로젝트는 이러한 의존성 모듈 버전을 package-lock.json이라는 파일을 통해 고정시켜버린다.
그 이유는 같은 모듈일지라도 모듈 버전에 따라 결과물이 달라진다거나 또는 에러가 날 수 있기 때문이다.
때문에 그런 에러를 줄여 함께 작업하는 사람들의 시간을 단축시키기 위해 package-lock.json을 사용한다.

(.nvmrc 파일이 하는 역할도 같은 맥락이다. nvm을 사용하는 개발자에게 노드 버전을 명시하는 역할을 한다.)

모듈 관리툴로 yarn을 사용한다면 yarn.lock 파일이 package-lock.json 역할을 대신한다.

하지만 package-lock.json을 통해 모듈 버전을 고정시키게 되면 장기간 모듈 버전이 업데이트 안되는 현상이 발생한다.
그렇게 되면 해당 프로젝트에 사용하고 있는 모듈의 취약점들이 해결이 안된채로 남아있게 된다.
그래서 주기적으로 package-lock.json도 업데이트 해줘야된다.

하지만 package-lock.json자동으로 설치 시점의 환경을 기록하는 파일이기 때문에 수동으로 변경해도 해결이 안된다.

설치 시점의 환경이란 package-lock.json 파일이 없는 상황에서 설치된 시점을 말한다.

5. 해결방법

npm i

레포를 클론받아 위 명령어를 통해 모듈 설치를 한다면, package.json(package-lock.json이 있다면 package-lock.json)에 명시되어 있는 모듈 버전을 설치한다.
그런데 설치하다보면 아래와 같은 메시지가 뜰 때가 있다.

MINGW64 /c/workspace/summer/foreign_survey/client (develop)
$ npm i
audited 30197 packages in 12.466s
found 657 vulnerabilities (574 low, 64 moderate, 16 high, 3 critical)
  run `npm audit fix` to fix them, or `npm audit` for details

npm은 npm i 명령어를 실행할 때 package의 취약점을 검사한다.
즉, 현재 취약점을 몇 개 찾았으니 npm audit fix 명령어를 통해 이 취약점을 해결하던지 npm audit 명령어를 통해 상세 내용을 확인하던지 해라. 라는 뜻이다.

npm audit 명령어는 npm 6 버전 이상에 추가된 기능이라고 한다.
원래는 다른 라이브러리가 이를 대신했었다고 한다.
현재는 npm 5 버전에도 위 기능을 추가한 상태라고 한다.

즉, 아래와 같이 명령어를 실행하면 취약점이 발견된 모듈을 자동으로 update 해준다.

MINGW64 /c/workspace/summer/foreign_survey/client (develop)
$ npm audit fix
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.1.3 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ babel-preset-env@1.7.0
added 43 packages from 39 contributors, removed 10 packages, updated 8 packages and moved 1 package in 28.66s
fixed 477 of 657 vulnerabilities in 30197 scanned packages
  7 package updates for 180 vulns involved breaking changes
  (use `npm audit fix --force` to install breaking changes; or do it by hand)

하지만 npm audit fix을 해도 취약점이 다 해결되지 않는 경우가 많다.
그럴 경우엔 아래 명령어로 현재 모듈들의 버전을 체크해준다.

USER@DESKTOP-RUCOU5S MINGW64 /d/
$ npm outdated
Package            Current  Wanted  Latest  Location
@babel/core         7.11.0  7.11.6  7.11.6  
@babel/preset-env   7.11.0  7.11.5  7.11.5  
@babel/register     7.10.5  7.11.5  7.11.5  
autoprefixer         9.8.6   9.8.6  10.0.0  
gulp-load-plugins    2.0.3   2.0.4   2.0.4  

그리고 각 모듈의 버전을 확인하여 최신 버전으로 업데이트 해준다.
그리고 다시 npm audit fix 명령어를 실행한다.
이렇게 하면 현재 취약점이 발견된 모듈 개수가 줄어든 것을 확인할 수 있을 것이다.

하지만 여전히 취약점이 있는 모듈이 있는 경우가 있다..
이럴 경우에 다음 명령어를 실행한다.

 npm audit

                       === npm audit security report ===

# Run  npm update lodash --depth 5  to resolve 13 vulnerabilities

  Low             Prototype Pollution

  Package         lodash

  Dependency of   browser-sync [dev]

  Path            browser-sync > easy-extender > lodash

  More info       https://npmjs.com/advisories/1523




  Low             Prototype Pollution

  Package         lodash

  Dependency of   cheerio [dev]

  Path            cheerio > lodash

  More info       https://npmjs.com/advisories/1523

  Low             Prototype Pollution

  Package         lodash

  Dependency of   gulp-htmlhint [dev]

  Path            gulp-htmlhint > htmlhint > async > lodash

  More info       https://npmjs.com/advisories/1523




  Low             Prototype Pollution

  Package         lodash

  Dependency of   gulp-htmlhint [dev]

  Path            gulp-htmlhint > htmlhint > jshint > lodash

  More info       https://npmjs.com/advisories/1523

위 명령어를 실행하면 취약점을 가지고 있는 모듈들 목록이 나온다.

  1. 모듈 이름과
  2. 해당 모듈이 어떤 문제를 발생시킬 수 있는지,
  3. 그리고 의존성이 어떻게 되는지
  4. 이러한 문제점을 해결하기 위한 정보를 제공해주는 Moro info url도 제공한다.

우선 맨 위에 쓰여있는 명령어를 실행해 취약점을 해결한다.

npm update lodash --depth 5

그래도 끝내 해결 안되는 모듈들은 아래처럼 하면 된다.
package.json에 끝내 해결 안되는 모듈들을 명시하자.

{
  "scripts": {
    "preinstall": "npx npm-force-resolutions"
  },
  "resolutions": {
    "yargs-parser": "^18.1.2",
    "minimist": "^1.2.3",
    "braces": "^2.3.1",
    "cryptiles": "^4.1.2",
    "stringstream": "^0.0.6",
    "hoek": "^5.0.3",
    "kind-of": "^6.0.3",
    "jpeg-js": "^0.4.0",
    "lodash.template": "^4.5.0"
  },
}

“scripts” 키는 커스텀 명령어를 작성할 수 있는 키이다.
위와 같이 커스텀 명령어를 입력하면

npm run preinstall

이란 명령어를 통해 npm npm-force-resolutions 명령어를 실행할 수 있다.
“resolutions” 키엔 npm audit 에서 나온 취약점을 갖고있는 모듈 목록을 넣어주면 된다.
옆에는 권장버전이다.
권장 버전은 아래처럼 깃헙에서도 확인할 수 있고,

More info에서도 확인할 수 있다.
위와 같이 package.json을 작성해주시고

npm run preinstall

명령어를 실행해주면 취약점을 갖고있는 모듈 문제가 해결된다.

주의 : 기존 사용하던 툴이 제대로 동작하는지 확인해봐야된다. 걸프, 그런트, 웹팩 등등