实现 NPM 依赖安全审计工具
安全依赖审计是指对某个工程所有的直接依赖或间接依赖进行安全性检查,发现潜在的风险。
何时需要审计
- 技术选型:针对目标技术(如第三方库)、远程审计(远程仓库,比如 github)
- 项目开发:针对当前工程(前期,中期和后期分别进行审计),本地审计(本地仓库)/远程审计(比如 gitlab)
审计的方式
- 使用
npm audit命令 - 自定义实现审计功能
NPM Audit 方式
最简单的方式是使用 npm audit 命令来进行安全依赖审计,但存在以下问题:
注意:需要切换到官方镜像
https://registry.npmjs.org,否则会出现404错误
- 阅读不友好:审计结果输出依赖关系不清晰
- 功能不完整:无法对远程仓库进行审计,无法对工程本身进行审计(只能审计依赖)
- 不方便集成:AI 应用是否支持运行命令,CI/CD 无法定义部署决策逻辑(输出的格式需要各种处理)
以下当前工程是使用 npm audit 命令进行安全依赖审计的结果
js
# npm audit report
@eslint/plugin-kit <0.3.4
@eslint/plugin-kit is vulnerable to Regular Expression Denial of Service attacks through ConfigCommentParser - https://github.com/advisories/GHSA-xffm-g5w8-qvg7
fix available via `npm audit fix`
node_modules/@eslint/plugin-kit
eslint 9.10.0 - 9.26.0
Depends on vulnerable versions of @eslint/plugin-kit
node_modules/eslint
brace-expansion 1.0.0 - 1.1.11 || 2.0.0 - 2.0.1
brace-expansion Regular Expression Denial of Service vulnerability - https://github.com/advisories/GHSA-v6h2-p8h4-qcjw
brace-expansion Regular Expression Denial of Service vulnerability - https://github.com/advisories/GHSA-v6h2-p8h4-qcjw
fix available via `npm audit fix`
node_modules/@eslint/config-array/node_modules/brace-expansion
node_modules/@eslint/eslintrc/node_modules/brace-expansion
node_modules/brace-expansion
node_modules/eslint/node_modules/brace-expansion
esbuild <=0.24.2
Severity: moderate
esbuild enables any website to send any requests to the development server and read the response - https://github.com/advisories/GHSA-67mh-4wv8-2f99
fix available via `npm audit fix`
node_modules/esbuild
vite <=6.1.6
Depends on vulnerable versions of esbuild
node_modules/vite
nanoid <3.3.8
Severity: moderate
Predictable results in nanoid generation when given non-integer values - https://github.com/advisories/GHSA-mwcw-c2x4-8c55
fix available via `npm audit fix`
node_modules/nanoid
6 vulnerabilities (3 low, 3 moderate)
To address all issues, run:
npm audit fix自定义实现方式
该功能可支持:
- 对本地工程或远程仓库均可进行审计
- 安全审计时能够对工程本身进行审计
- 审计结果中包含清晰的依赖关系路径
- 审计结果是一个标准的 markdown 格式文档
- 支持更多的扩展功能...
实现流程
创建工作目录:创建一个临时的工作目录,用于保存执行期间需要用到的临时文件
- 需要考虑目录的 uuid 命名,以便于删除
解析工程:解析本地工程目录或远程仓库链接,得到对应的
package.json文件内容- 需要区分本地工程和远程仓库,远程仓库可能需要调用 API 获取默认分支,然后再获取
package.json文件内容
- 需要区分本地工程和远程仓库,远程仓库可能需要调用 API 获取默认分支,然后再获取
生成 lock 文件:将
package.json写入到临时目录,同时根据它生成package-lock.json文件- 使用
npm install --package-lock-only生成package-lock.json文件
- 使用
安全审计:进入到临时工作目录,使用
npm audit命令进行安全审计,并将审计结果规格化- 如何获取审计结果:使用
npm audit --json - 审计结果包含的信息
severity: 严重程度 https://docs.npmjs.com/about-audit-reports#severity
source: npm 对漏洞的编号,仅存在于 npm 包中的漏洞
CVE: 漏洞的通用编号,该编码跨越语言,可以从 https://www.cve.org 查询详情
CWE:漏洞类型编码,通过此编码可以找到漏洞是如何产生的,会造成什么影响,可以通过 https://cwe.mitre.org 查询详情
CVSS:漏洞严重性评分,对应到 severity 字段 - 规格化的目标
- 如何实现规格化:图的 DFS 算法
- 如何获取当前工程的审计结果:npm 的远程 API
- 把当前工程的审计结果汇总到结果中
- 如何获取审计结果:使用
渲染:将上一步得到的规格化审计结果进行渲染,渲染成标准化的
markdown内容,并保存到结果文件- 如何将审计结果渲染成 markdown 格式:使用模板引擎,如
ejs或handlebars等
- 如何将审计结果渲染成 markdown 格式:使用模板引擎,如
删除工作目录:将之前创建的临时工作目录删除
以下当前工程是使用 npm audit --json 的输出结果,上面所有流程都是基于这个结果进行的
js
{
"auditReportVersion": 2, // 版本号
"vulnerabilities": {
"@eslint/plugin-kit": {
"name": "@eslint/plugin-kit",
"severity": "low",
"isDirect": false,
"via": [
{
"source": 1106734,
"name": "@eslint/plugin-kit",
"dependency": "@eslint/plugin-kit",
"title": "@eslint/plugin-kit is vulnerable to Regular Expression Denial of Service attacks through ConfigCommentParser",
"url": "https://github.com/advisories/GHSA-xffm-g5w8-qvg7",
"severity": "low",
"cwe": [
"CWE-1333"
],
"cvss": {
"score": 0,
"vectorString": null
},
"range": "<0.3.4"
}
],
"effects": [
"eslint"
],
"range": "<0.3.4",
"nodes": [
"node_modules/@eslint/plugin-kit"
],
"fixAvailable": true
},
"brace-expansion": {
"name": "brace-expansion",
"severity": "low",
"isDirect": false,
"via": [
{
"source": 1105443,
"name": "brace-expansion",
"dependency": "brace-expansion",
"title": "brace-expansion Regular Expression Denial of Service vulnerability",
"url": "https://github.com/advisories/GHSA-v6h2-p8h4-qcjw",
"severity": "low",
"cwe": [
"CWE-400"
],
"cvss": {
"score": 3.1,
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L"
},
"range": ">=1.0.0 <=1.1.11"
},
{
"source": 1105444,
"name": "brace-expansion",
"dependency": "brace-expansion",
"title": "brace-expansion Regular Expression Denial of Service vulnerability",
"url": "https://github.com/advisories/GHSA-v6h2-p8h4-qcjw",
"severity": "low",
"cwe": [
"CWE-400"
],
"cvss": {
"score": 3.1,
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L"
},
"range": ">=2.0.0 <=2.0.1"
}
],
"effects": [],
"range": "1.0.0 - 1.1.11 || 2.0.0 - 2.0.1",
"nodes": [
"node_modules/@eslint/config-array/node_modules/brace-expansion",
"node_modules/@eslint/eslintrc/node_modules/brace-expansion",
"node_modules/brace-expansion",
"node_modules/eslint/node_modules/brace-expansion"
],
"fixAvailable": true
},
"esbuild": {
"name": "esbuild",
"severity": "moderate",
"isDirect": false,
"via": [
{
"source": 1102341,
"name": "esbuild",
"dependency": "esbuild",
"title": "esbuild enables any website to send any requests to the development server and read the response",
"url": "https://github.com/advisories/GHSA-67mh-4wv8-2f99",
"severity": "moderate",
"cwe": [
"CWE-346"
],
"cvss": {
"score": 5.3,
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N"
},
"range": "<=0.24.2"
}
],
"effects": [
"vite"
],
"range": "<=0.24.2",
"nodes": [
"node_modules/esbuild"
],
"fixAvailable": true
},
"eslint": {
"name": "eslint",
"severity": "low",
"isDirect": true,
"via": [
"@eslint/plugin-kit"
],
"effects": [],
"range": "9.10.0 - 9.26.0",
"nodes": [
"node_modules/eslint"
],
"fixAvailable": true
},
"nanoid": {
"name": "nanoid",
"severity": "moderate",
"isDirect": false,
"via": [
{
"source": 1101163,
"name": "nanoid",
"dependency": "nanoid",
"title": "Predictable results in nanoid generation when given non-integer values",
"url": "https://github.com/advisories/GHSA-mwcw-c2x4-8c55",
"severity": "moderate",
"cwe": [
"CWE-835"
],
"cvss": {
"score": 4.3,
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N"
},
"range": "<3.3.8"
}
],
"effects": [],
"range": "<3.3.8",
"nodes": [
"node_modules/nanoid"
],
"fixAvailable": true
},
"vite": {
"name": "vite",
"severity": "moderate",
"isDirect": false,
"via": [
{
"source": 1102437,
"name": "vite",
"dependency": "vite",
"title": "Websites were able to send any requests to the development server and read the response in vite",
"url": "https://github.com/advisories/GHSA-vg6x-rcgg-rjx6",
"severity": "moderate",
"cwe": [
"CWE-346",
"CWE-350",
"CWE-1385"
],
"cvss": {
"score": 6.5,
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"
},
"range": ">=5.0.0 <=5.4.11"
},
{
"source": 1103517,
"name": "vite",
"dependency": "vite",
"title": "Vite bypasses server.fs.deny when using ?raw??",
"url": "https://github.com/advisories/GHSA-x574-m823-4x7w",
"severity": "moderate",
"cwe": [
"CWE-200",
"CWE-284"
],
"cvss": {
"score": 5.3,
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N"
},
"range": ">=5.0.0 <5.4.15"
},
{
"source": 1103628,
"name": "vite",
"dependency": "vite",
"title": "Vite has a `server.fs.deny` bypassed for `inline` and `raw` with `?import` query",
"url": "https://github.com/advisories/GHSA-4r4m-qw57-chr8",
"severity": "moderate",
"cwe": [
"CWE-200",
"CWE-284"
],
"cvss": {
"score": 5.3,
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N"
},
"range": ">=5.0.0 <5.4.16"
},
{
"source": 1103884,
"name": "vite",
"dependency": "vite",
"title": "Vite has an `server.fs.deny` bypass with an invalid `request-target`",
"url": "https://github.com/advisories/GHSA-356w-63v5-8wf4",
"severity": "moderate",
"cwe": [
"CWE-200"
],
"cvss": {
"score": 0,
"vectorString": null
},
"range": ">=5.0.0 <5.4.18"
},
{
"source": 1104173,
"name": "vite",
"dependency": "vite",
"title": "Vite's server.fs.deny bypassed with /. for files under project root",
"url": "https://github.com/advisories/GHSA-859w-5945-r5v3",
"severity": "moderate",
"cwe": [
"CWE-22"
],
"cvss": {
"score": 0,
"vectorString": null
},
"range": ">=5.0.0 <=5.4.18"
},
{
"source": 1104202,
"name": "vite",
"dependency": "vite",
"title": "Vite allows server.fs.deny to be bypassed with .svg or relative paths",
"url": "https://github.com/advisories/GHSA-xcj6-pq6g-qj4x",
"severity": "moderate",
"cwe": [
"CWE-200",
"CWE-284"
],
"cvss": {
"score": 5.3,
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N"
},
"range": ">=5.0.0 <5.4.17"
},
{
"source": 1107323,
"name": "vite",
"dependency": "vite",
"title": "Vite middleware may serve files starting with the same name with the public directory",
"url": "https://github.com/advisories/GHSA-g4jq-h2w9-997c",
"severity": "low",
"cwe": [
"CWE-22",
"CWE-200",
"CWE-284"
],
"cvss": {
"score": 0,
"vectorString": null
},
"range": "<=5.4.19"
},
{
"source": 1107327,
"name": "vite",
"dependency": "vite",
"title": "Vite's `server.fs` settings were not applied to HTML files",
"url": "https://github.com/advisories/GHSA-jqfw-vq24-v9c3",
"severity": "low",
"cwe": [
"CWE-23",
"CWE-200",
"CWE-284"
],
"cvss": {
"score": 0,
"vectorString": null
},
"range": "<=5.4.19"
},
"esbuild"
],
"effects": [],
"range": "<=6.1.6",
"nodes": [
"node_modules/vite"
],
"fixAvailable": true
}
},
"metadata": {
"vulnerabilities": {
"info": 0,
"low": 3,
"moderate": 3,
"high": 0,
"critical": 0,
"total": 6
},
"dependencies": {
"prod": 1,
"dev": 506,
"optional": 8,
"peer": 6,
"peerOptional": 0,
"total": 506
}
}
}