Skip to content
大刘 大刘 发布于 2025年09月13日
一个 web 开发者

实现 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 格式文档
  • 支持更多的扩展功能...

实现流程

  1. 创建工作目录:创建一个临时的工作目录,用于保存执行期间需要用到的临时文件

    • 需要考虑目录的 uuid 命名,以便于删除
  2. 解析工程:解析本地工程目录或远程仓库链接,得到对应的 package.json 文件内容

    • 需要区分本地工程和远程仓库,远程仓库可能需要调用 API 获取默认分支,然后再获取 package.json 文件内容
  3. 生成 lock 文件:将 package.json 写入到临时目录,同时根据它生成 package-lock.json 文件

    • 使用 npm install --package-lock-only 生成 package-lock.json 文件
  4. 安全审计:进入到临时工作目录,使用 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
    • 把当前工程的审计结果汇总到结果中
  5. 渲染:将上一步得到的规格化审计结果进行渲染,渲染成标准化的 markdown 内容,并保存到结果文件

    • 如何将审计结果渲染成 markdown 格式:使用模板引擎,如 ejshandlebars
  6. 删除工作目录:将之前创建的临时工作目录删除

以下当前工程是使用 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
    }
  }
}

基于 MIT 许可发布