依赖包漏洞扫描的工作流程大致如下图,通过这个图我们先对整个过程有个粗略的印象。本文会先扫扫盲,介绍一些常见的名词,然后再介绍几个漏洞扫描工具,最后以其中一个工具作为示例,介绍如何进行 npm 依赖包漏洞检测和修复。
与漏洞有关的常见关键词
无论是一份漏洞扫描报告中,还是在具体某个安全漏洞的详请介绍中,下面几个名词常看到,所以在进一步了解漏洞扫描前,我们有必要先进行简单地扫盲。
CVE
CVE是通用漏洞披露(Common Vulnerabilities and Exposures) 的简称,是一个记录常见漏洞的资料库。CVE对每一个漏洞都会有一个专属的编号,格式为CVE-YYYY-NNNNN。YYYY为漏洞披露年份,NNNNN为流水编号,例如CVE-2021-46101
。如下图为CVE各个年份记录的漏洞数:
CVSS
CVSS是通用漏洞评分系统(Common Vulnerability Scoring System) 的简称。CVSS 提供了对漏洞进行打分的标准,是一个打分系统。CVSS 首先从各个维度对一个漏洞进度量,然后结合各个度量值的答案得出一个综合分数,分数越高越严重。CVSS的评分有三个指标组:基本分、时间分和环境分。(下面图示来自 v3.1标准)
基本分:代表每个漏洞的固有特征,度量值包括攻击向量(AV)、攻击复杂度(AC)、权限要求(PR)等, 共8个。如下图,综合各度量值所选得出的基本分为6.4:
时间分 :由于外部的事件随时间变化的漏洞特性,共3个度量值。时间分是在得出基本分的前提下,对漏洞进行更进一步的打分。如下图,在基本分为6.4的前提下,若修复水平(RL)为官方修复(O),则时间分为6.1:
环境分:反映漏洞与用户环境相关的危害性,共11个度量值。环境分是在得出基本分和时间分的前提下,对漏洞进行再进一步的打分。如下图,在基本分为6.4和时间为6.1的前提下,若保密性要求(CR)为L,则环境分为5.5:
根据公式,每个指标综合各个度量值的答案,计算出这个指标的分数, 公式计算方法在这里,有兴趣可以看看。漏洞的基本分可按分级标准进行定级(例如低、中和高)。如下图为该系统两个不同版本(v2.0 和 v3.0)的分级标准:
如下图示例,对于漏洞 CVE-2013-0375:MySQL Stored SQL Injection 的评分,两个版本的度量值有所差异,得出的分数也不同,基于 v2.0 基础分为5.5,基于v3.1 基础分为6.4。
CWE
CWE是通用缺陷列表(Common Weakness Enumeration) 的简称,是一个对软件脆弱性和易受攻击性的分类系统, 提供了一份软件弱点类型列表,(CWE的角色个人觉得可理解为类似 HTTP 状态码)。如下图部分弱点分类:
CPE
CPE是通用枚举平台(Common Platform Enumeration) 的简称,是一种标准化方法,用来描述漏洞影响了哪些应用程序/组件的哪些版本。格式形如 CPE:2.3:类型:厂商:产品:版本:更新版本:发行版本:界面语言:软件发行版本:目标软件:目标硬件:其他 , 其中2.3为CPE的版本。例如,cpe:2.3:a:*:react-dev-utils:>=0.4.0<11.0.4:*:*:*:*:*:*:*
。
其他漏洞数据库
CNNVD 是中国国家信息安全漏洞库,于2009年10月18日正式成立。NVD 是美国国家漏洞数据库,创建于2000年。如下图,以 CVE-2021-46101
这个漏洞在 NVD 和 CNNVD 的记录为例,在 NVD 中会展示该漏洞的 CVSS 基本分、所属的 CWE 类别和涉及的 CPE 信息。
扫描工具
依赖包漏洞检测的开源工具和软件比较多,这里不展开详细介绍,本文只介绍三种 npm 包自动化检测工具npm audit
、snyk
和 Dependency-Check
。
npm audit
npm v6.x 新增 npm-audit 命令。npm audit
的漏洞数据来自 GitHub Advisory Database。npm audit
对第三方包的扫描依赖于 package.json 和 package-lock.json 文件,如果没有这两个文件会报错。执行npm audit
命令后,会输出如下图的安全报告,报告存在漏洞的依赖包信息。可以执行 npm audit fix
进行修复,对于非兼容性的依赖包升级需要执行 npm audit fix --force
。
Snyk
Snyk 是一家美国的安全公司,帮助开发者们在开发阶段就能查找、修复和监测代码的脆弱性。Snyk 维护自己的开源漏洞数据库。Snyk 可以直接登录 github 账号,然后选择要扫描的 github 仓库。如下图,我扫描了仓库下的 EasyPro
和 vuePro
项目,扫描结果显示 vuePro
项目下有漏洞,其中,npm 包涉及的漏洞有四个:一个高危三个中度。
点击查看漏洞详情,如下图,显示其中3个漏洞均来自axios@0.16.2
,一个漏洞来自async-validator@1.4.13
。
让我觉得眼前一亮的功能是,Snyk 针对这些依赖包漏洞,能够自动提交 PR(Pull requests),如下图,一顿点击猛如虎后,Snyk 提交了一个 PR [Snyk] Security upgrade axios from 0.16.2 to 0.21.3
到项目中。
Dependency-Check
Dependency-Check 是 OWASP 的一个实用开源程序,基于 NVD 漏洞数据库检查项目依赖项。它主要用于扫描 Java 和 .NET 代码。另外它也支持扫描 Python,Ruby,PHP(Composer)和 Node.js 代码,但这些语言的扫描都是实验性的,可能会有误报。
Dependency-Check 通过收集依赖包的信息来发现漏洞,收集的信息称为 Evidence
, 包括三种:提供者信息 (vendor)、依赖包名称 (product) 和依赖包版本(version)。通过将 Evidence 和 CPE 条目进行匹配,匹配到了就会给个标志发送到报告中,记录一个依赖包漏洞。对于前端 npm
包的扫描,与 npm audit
和 snyk
不同,Dependency-Check 是直接扫描文件夹下存在的依赖包, 然后输出一份 .html
格式的漏洞报告,它不依赖 package.json
和 package-lock.json
信息。
实践:第三方包漏洞检测并修复
下文将以 Dependency-Check 命令行工具为例,详细介绍如何扫描一个前端项目中的依赖包漏洞,并根据报告进行漏洞修复,即对 npm 包进行指定版本升级。
1. 下载 Dependency-Check 工具
tips:运行前要准备好 java 环境,如果没有,会爆错。 java 安装可以参考这篇。
2. 执行命令,生成报告
解压后, 执行命令:
cd bin
dependency-check.bat --disableNodeJS --project test -s F:choprrrr-app -o F:choprrrr-appdependency-check-report
- --project
:工程名称
- -s
:要扫描的文件夹
- -o
:结果输出路径
- --disableNodeJS
:不检查 nodejs
运行命令后,工具会先开始下载各个年份 NVD 漏洞数据库:
下载完成后开始扫描,并将扫描结果输出到指定的路径中。
在第一次扫描后,会将下载的各个年份的 NVD 漏洞数据库缓存到工具 data/nvdcache
的文件夹中。当再次扫描其他项目时,会校验数据库是否有更新,如果没有更新,会从缓存读取 NVD 漏洞数据库数据,然后直接开始扫描不需要重新下载。
3. 解析报告
报告主要包括三个部分:
基本信息: 包括项目名、工具版本信息,依赖包数量和有漏洞的依赖包数量。
Summary: 以表格列出所有有风险的依赖包版本、风险等级、涉及的漏洞数量和 Evidence
数量。
Dependencies: 每个依赖包涉及的漏洞的具体说明。如下图,以 ansi-html@0.0.7
这个依赖包为例,
4. 调整npm包版本
发现漏洞后,升级 npm 包版本,乍一听好像并不困难,但是有两个问题需要考虑: 1)升级到哪个版本;2)针对非直接引入的依赖,如何锁定版本。
升级到哪个版本?
虽然漏洞报告已经很清楚描述了漏洞涉及的版本以及每个漏洞修复的版本,但是为了更加精确且开发成本最小地升级到没有任何漏洞的版本,我们可以借助 snyk 网站进行查询,找到适合自己项目的版本进行安装。因为在实际项目中,我们不仅要考虑该版本是否有漏洞,还要考虑配置兼容性等问题。 例如,报告显示 webpack-dev-server@2.11.5
存在漏洞,通过查询我们得知, 至少要升级到大版本 webpack-dev-server@3.1.11
才能“完全”没有任何漏洞,但是 webpack-dev-server@3+
是基于webpack@4+
的,所以如果项目是 webpack@3+
也要升级大版本。
当然,上面说的要升级大版本的问题,是因为 webpack-dev-server@2+
后面没有出一个 hotfix 的小版本修复这个漏洞,(偏一下题,为啥没有针对这个漏洞的 hotfix 小版本呢?我找到的官方答复有兴趣地可以瞅瞅)。有些 npm 包针对低版本的漏洞会出 hotfix 小版本去修复漏洞,那我们就可以借助这个网站查到对应版本,快速地进行小版本升级。
BTW:上面截图的针对某个 npm 包各个版本漏洞信息的表格,暂时没找到入口,但是我发现可以直接修改链接地址进行查询,类似这样https://snyk.io/vuln/npm:${npm包名}
,以上图 webpack-dev-server为例。
针对非直接引入的依赖,如何锁定版本?
1)整理想要指定版本的依赖,在 package.json
中新加 resolutions
字段。
对于非直接引入的 npm 包,除了指定安装版本外,有时候因为原代码库不再维护等原因,我们希望替换包,也可以放在这里配置。如下配置为,将 ansi-html
替换成 ansi-html-community
和将 acorn
锁定版本 v7.4.0
。
"resolutions": {
"ansi-html": "http://190.xx.xx.xx:xxxx/repository/npm-group/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
"acorn": "7.4.0",
}
2)在 package.json
中的 scripts
字段增加 preinstall
, 它的作用是在运行 npm install
前就会先自动运行 preinstall
指令,生成新的package-lock.json
。
{
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js",
"preinstall": "npm install --package-lock-only --ignore-scripts && npx npm-force-resolutions@0.0.3"
},
}
3)删除项目已有的 package-lock.json
和 node_modules
文件夹, 运行npm install
,如下图,为锁定前后这两个包的 package-lock.json
信息。
4)通过以上步骤后,其实我们已经生成锁定版本的package-lock.json
,为了避免再次安装的时候,再走一遍冗余的流程,其实我们可以将 [步骤2] 新加的 preinstall
字段去掉。
参考资料