依赖包安全管理

中等 🟡前端安全
15 个标签
预计阅读时间:38 分钟
前端安全依赖包安全审计漏洞管理供应链安全npmSCASBOMCVSSEPSS依赖混淆typosquattingprovenance许可证合规OpenSSF Scorecard

依赖包安全管理

现代前端项目就像用乐高搭房子:真正自己写的代码往往只占一小部分,剩下的绝大多数由 npm 上的第三方"积木"拼成。一个中等规模的项目 `node_modules` 里动辄有上千个包,其中很多是你根本没直接引用、却被间接拖进来的"依赖的依赖"。只要其中任何一块积木存在漏洞或被人做了手脚,整栋房子都可能塌方。这就是"软件供应链安全"要解决的问题。

据 Sonatype 的年度报告,开源组件的下载量每年以数倍速度增长,针对开源供应链的攻击也随之激增。一个应用 80% 以上 的代码来自第三方依赖,因此依赖安全早已不是"锦上添花",而是"守住底线"。

依赖包安全风险

常见漏洞类型:

原型链污染(Prototype Pollution)
ReDoS(正则表达式拒绝服务)
代码注入 / 命令注入
通过依赖引入的 XSS / CSRF
权限提升与信息泄露

风险来源:

直接依赖的已知漏洞
间接(传递)依赖的漏洞——最容易被忽视,占比往往超过一半
长期不更新导致的过时版本
恶意包:投毒、typosquatting(仿冒热门包名)
供应链攻击:维护者账号被盗、CI 被入侵后发布带后门的版本

影响范围: 应用被入侵、用户数据泄露、服务不可用、企业声誉与合规风险。

为什么依赖安全重要

你可以把自己的代码写得滴水不漏,但只要 `npm install` 拉进来一个带后门的包,一切防线瞬间失效——因为它和你的代码运行在同一进程、同样的权限下。攻击者深知这一点,所以近年来越来越多地"攻击上游":与其攻破一个应用,不如污染一个被上万个应用依赖的底层库,一次得手,处处沦陷。

真实案例

案例一:left-pad 事件(2016)

一位作者一怒之下把只有 11 行代码的 `left-pad` 从 npm 撤下,结果依赖它的 Babel、React 等大量项目瞬间构建失败,半个前端生态"停摆"数小时。它暴露了"依赖过度碎片化 + 无锁定"的脆弱性,也促成了 npm 禁止随意 unpublish 的政策。

案例二:event-stream 投毒(2018)

热门包 `event-stream` 的维护权被转交给一位"热心贡献者",对方在其中植入恶意代码,专门窃取特定比特币钱包应用的私钥。由于 `event-stream` 被海量项目间接依赖,恶意代码悄悄扩散到无数下游。它是"维护者社会工程攻击"的经典教材。

案例三:Log4Shell(CVE-2021-44228)

Java 生态的 Log4j 爆出可远程执行代码的严重漏洞,CVSS 评分高达 10.0(满分)。全球无数系统受影响,很多团队甚至不知道自己"间接"用了 Log4j。它让"你必须清楚知道自己依赖了什么"(软件物料清单 SBOM)成为行业共识。

案例四:颜色库投毒(colors.js / faker.js,2022)

作者主动在 `colors.js` 中加入死循环,导致大量使用它的应用打印乱码并卡死。这属于"维护者自毁"式风险,提醒我们即便是可信的老牌库也需要锁定版本、审慎升级。

依赖安全工具

漏洞扫描(SCA):

`npm audit` / `yarn audit` / `pnpm audit`:包管理器内置,零成本起步
Snyk:专业 SCA 平台,漏洞库更全,提供自动修复 PR
OWASP Dependency-Check:开源,适合接入 CI
Socket.dev:侧重"恶意行为"检测(是否偷偷联网、读环境变量等)

自动更新:

Dependabot(GitHub 原生):自动提交依赖升级 PR
Renovate:更灵活的自动更新,支持分组、调度策略

静态分析与监控:

CodeQL:GitHub 的代码查询引擎,可发现代码级安全问题
GitHub Security Alerts:依赖出现已知漏洞时自动告警
Snyk Monitor:持续监控,发现新披露漏洞即通知

代码示例

示例 1:命令行审计与修复

bashCode
# 扫描当前项目依赖的已知漏洞
npm audit

# 只看高危及以上
npm audit --audit-level=high

# 自动修复(在语义化版本允许范围内升级)
npm audit fix

# 强制修复(可能引入破坏性大版本,需回归测试)
npm audit fix --force

# 以 JSON 输出,便于脚本处理
npm audit --json > audit-report.json

示例 2:在 CI 中设置安全门禁

yamlCode
# .github/workflows/security.yml
name: Dependency Security
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      # 出现 high 及以上漏洞则让流水线失败,阻止合并
      - run: npm ci
      - run: npm audit --audit-level=high

示例 3:用 overrides 强制锁定间接依赖的安全版本

jsonCode
{
  "name": "my-app",
  "overrides": {
    "lodash": "4.17.21",
    "semver": ">=7.5.2",
    "postcss@<8.4.31": "8.4.31"
  }
}

示例 4:Dependabot 配置

yamlCode
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    groups:
      # 把补丁级更新合并成一个 PR,减少噪音
      patch-updates:
        update-types: ["patch"]

示例 5:安装前的最小化安全检查(防投毒)

bashCode
# 查看包的元信息,警惕新发布、下载量极低、仿冒名
npm view suspicious-pkg

# 禁止安装脚本自动执行(很多投毒通过 postinstall 触发)
npm install some-pkg --ignore-scripts

# 使用 npm ci 保证严格按 lock 文件安装,杜绝意外版本漂移
npm ci

# 生成软件物料清单(SBOM),便于漏洞追溯
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

依赖管理最佳实践

依赖选择:

优先选活跃维护、社区大、发布规律的包
查看 GitHub star、issue 响应、最近提交时间
引入前评估"是否真的需要"——能自己几行代码搞定的就别装包
警惕 typosquatting:仔细核对包名拼写

版本管理:

生产依赖尽量精确锁定,避免宽泛的 `^` / `*` 带来意外升级
但也不能永不升级——用 Dependabot/Renovate 保持小步快跑
升级后必须跑回归测试

依赖锁定:

必须提交 `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`
CI 用 `npm ci` 而非 `npm install`,保证可复现构建
lock 文件让审计和回滚有据可依

安全审计:

定期(如每周)跑扫描,接入 CI 门禁
按严重程度分级处理,记录处置过程
维护 SBOM,做到"心里有数"

漏洞修复策略

| 严重程度 | 响应时限 | 处理方式 |

| --- | --- | --- |

| 紧急/严重(Critical) | 立即(小时级) | 马上升级或临时替换、下线受影响功能 |

| 高危(High) | 数天内 | 制定修复计划、测试后尽快部署 |

| 中危(Medium) | 常规迭代 | 纳入下个版本一并升级 |

| 低危(Low) | 择机 | 评估实际暴露面后适时处理 |

修复时优先看漏洞是否"实际可达"——有些漏洞存在于从未被调用的代码路径,实际风险较低,可结合 `npm audit` 的 reachability 信息与业务上下文判断优先级。

供应链安全

来源验证: 使用官方 registry;对内部包用私有 registry;开启 `npm` 的 provenance(发布溯源)校验。

构建环境安全: 隔离构建环境、固定基础镜像、扫描构建产物、CI 中最小化权限与 Secret 暴露。

依赖审查流程: 新引入依赖需评审(安全、许可证、维护度);建立依赖审批清单;关注 `postinstall` 等安装脚本。

常见坑

只看直接依赖,忽略传递依赖:大多数漏洞藏在"依赖的依赖"里,需用 `npm ls ` 追溯。
audit fix --force 引入破坏性升级:可能悄悄升到不兼容大版本,务必回归测试。
不提交 lock 文件:导致每次安装版本漂移,构建不可复现,也让审计失效。
盲目追新或永不更新:两个极端都危险,应小步稳更 + 门禁把关。
忽视安装脚本:投毒常借 `postinstall` 执行,可用 `--ignore-scripts` 缓解。
typosquatting:一个字母之差的仿冒包足以酿成事故,安装前核对包名与作者。
把审计当一次性任务:漏洞是持续披露的,昨天干净不代表今天安全,必须持续监控。

数据与对比

| 工具 | 类型 | 检测传递依赖 | 自动修复 | 适用场景 |

| --- | --- | --- | --- | --- |

| npm audit | 内置 SCA | 支持 | 有限 | 快速起步 |

| Snyk | 商业 SCA | 支持 | 强,出 PR | 企业级持续监控 |

| Dependabot | 自动更新 | 支持 | 出升级 PR | GitHub 项目 |

| OWASP Dep-Check | 开源 SCA | 支持 | 无 | 自建 CI 流水线 |

| Socket.dev | 行为检测 | 支持 | 无 | 防恶意包/投毒 |

漏洞数据库与资源

CVE:全球统一的漏洞编号系统(如 Log4Shell 为 CVE-2021-44228)
NVD:美国国家漏洞库,含 CVSS 评分与详情
GitHub Advisory Database:与生态深度集成的公告库
Snyk Vulnerability DB:更新及时、覆盖广
npm Security Advisories:npm 官方安全公告

软件供应链攻击分类学

要防住供应链攻击,先得看懂攻击者有哪些"进攻套路"。下面把常见的攻击手法逐类拆开,讲清"它是什么、怎么得手、怎么防"。

1. Typosquatting(仿冒包名)

攻击者注册一个和热门包极其相似的名字,赌你手滑打错。比如把 `lodash` 注册成 `lodahs`、`loadsh`,把 `crossenv` 冒充 `cross-env`。2017 年 npm 就下架过一批仿冒包,其中 `crossenv` 会在安装时把你的环境变量(含各种密钥)打包上传到攻击者服务器。防御靠:安装前逐字核对包名、用 `npm view ` 看真实作者与下载量、开启 Socket.dev 之类的行为检测。

2. Dependency Confusion(依赖混淆)

这是 2021 年被 Alex Birsan 发扬光大的攻击:企业内部私有包(如 `@company/internal-utils`)如果同时能从公共 registry 解析,包管理器往往会"谁版本号大就装谁"。攻击者只要在公共 npm 上抢注同名包并把版本号写成 `99.99.99`,你的 CI 就会误装公共的恶意版本。Birsan 用这招打进了 Apple、微软、特斯拉等 35 家公司的内网,累计拿到 13 万美元 漏洞赏金。防御靠:私有 scope 绑定私有 registry、配置 `.npmrc` 明确 scope 路由、开启 provenance 校验。

3. 恶意维护者 / 账号劫持

包本来是好的,但维护者"变质"或账号被盗。变质有两种:一是老维护者把权限转交给陌生人(`event-stream` 就是这样),二是维护者自己搞破坏(`colors.js` 死循环、`node-ipc` 抗议代码)。账号劫持则是攻击者通过撞库、钓鱼、或窃取 npm token 拿到发布权限后推恶意版本。防御靠:锁定版本 + 引入延迟观察期(见后文 minimumReleaseAge)、维护者应开启双因素认证。

4. Install Script 投毒

npm 包可以在安装时自动运行 `preinstall` / `install` / `postinstall` 脚本,这是投毒的最爱入口——你还没 import 它,代码就已经在你机器上跑了。恶意脚本常做:读取 `.env`、`~/.aws/credentials`、SSH 私钥并外传,或在 CI 里植入后门。防御靠:`.npmrc` 里设 `ignore-scripts=true`(对确需脚本的包再单独放行)。

5. 协议劫持 / 传输层攻击

包在下载途中被中间人替换,或 registry 本身被投毒缓存。防御靠:强制 HTTPS、校验包完整性哈希(lock 文件里的 `integrity` 字段就是干这个的)、企业用私有 registry 做统一代理与缓存。

6. 构建系统入侵

不攻击包本身,而是攻破生产该包的 CI/CD 流水线,在"正版"发布流程里注入后门。SolarWinds 和 Codecov 都属于此类,杀伤力最大——因为产物带着合法签名,下游几乎无从察觉。防御靠:构建环境隔离、最小权限、产物签名与 SLSA 溯源。

| 攻击类型 | 入口 | 典型案例 | 关键防御 |

| --- | --- | --- | --- |

| Typosquatting | 打错包名 | crossenv | 核对包名、行为检测 |

| 依赖混淆 | 版本号抢高 | Birsan 攻击 | 私有 registry 绑定 scope |

| 恶意维护者 | 权限转交/账号盗 | event-stream | 锁版本、观察期、2FA |

| install 脚本 | postinstall | crossenv | ignore-scripts |

| 协议劫持 | 传输中间人 | 缓存投毒 | HTTPS、integrity 校验 |

| 构建入侵 | CI/CD 被攻破 | SolarWinds | 隔离、签名、SLSA |

更多重磅真实案例

理论讲完了,看真实世界里这些攻击造成过多大破坏。带上数据和时间线,印象更深。

SolarWinds(2020)——供应链攻击的"珠峰"

攻击者(代号 UNC2452)攻破了 SolarWinds 的构建系统,在其网络管理软件 Orion 的正版更新包里植入名为 SUNBURST 的后门。约 1.8 万 家客户下载了带毒更新,受害者包括美国财政部、商务部、国土安全部等多个联邦机构。攻击潜伏了数月才被 FireEye 发现。它彻底改变了业界对供应链的认知,直接催生了美国总统 14028 号行政令,要求政府采购软件必须提供 SBOM。

ua-parser-js 投毒(2021)

这个每周下载量约 800 万次 的 UA 解析库,维护者 npm 账号被劫持,攻击者发布了 0.7.29、0.8.0、1.0.0 三个恶意版本,会下载挖矿程序和窃密木马。由于下载量巨大,npm 和 GitHub 紧急发出安全公告,要求所有用过这些版本的人视机器为已失陷、立即改密码。从投毒到被发现只有几个小时,但影响面极广。

node-ipc 抗议代码(2022,CVE-2022-23812)

维护者出于反战抗议,在这个被 `@vue/cli` 等大量项目间接依赖的包里加入代码:检测到 IP 属于俄罗斯或白俄罗斯时,会遍历文件系统并把文件内容覆写成心形符号(即"数据擦除")。这是"protestware"的标志性事件,说明即便是善意维护者也能瞬间变成破坏源,锁版本有多重要。

xz-utils 后门(2024,CVE-2024-3094,CVSS 10.0)

迄今最精心策划的开源后门之一。一个化名 Jia Tan 的账号花了近两年时间伪装成热心贡献者,逐步骗取 xz 压缩库的维护权,然后在 5.6.0 / 5.6.1 版本里植入后门,专门针对通过 systemd 链接了 liblzma 的 OpenSSH,可实现远程未授权 RCE。幸亏微软工程师 Andres Freund 因为 SSH 登录慢了 约 0.5 秒 而顺藤摸瓜发现,赶在主流发行版大规模采用前被拦下。它暴露了"长期社会工程 + 单点维护者"的致命脆弱。

PyTorch 依赖混淆(2022 末)

PyTorch 的 nightly 版本依赖一个名为 `torchtriton` 的包。攻击者在公共 PyPI 上抢注了同名包,利用依赖混淆让 nightly 用户误装恶意版本,其 `setup.py` 会窃取主机名、用户名、`.ssh` 与 `.gitconfig` 等敏感信息外传。PyTorch 团队紧急把 `torchtriton` 从 PyPI 拉黑并改用自有 index。

Codecov(2021)

覆盖率工具 Codecov 的 Bash Uploader 脚本被攻击者篡改(因其 Docker 镜像创建流程有缺陷),持续两个多月向攻击者外传使用者 CI 环境里的所有环境变量——也就是无数公司的密钥、token。数千家企业受影响,属于典型的构建/分发环节被入侵。

| 案例 | 年份 | 类型 | 关键数据 |

| --- | --- | --- | --- |

| SolarWinds | 2020 | 构建入侵 | 约 1.8 万客户受影响 |

| ua-parser-js | 2021 | 账号劫持 | 每周约 800 万下载 |

| node-ipc | 2022 | protestware | CVE-2022-23812 |

| xz-utils | 2024 | 长期渗透 | CVSS 10.0,潜伏约 2 年 |

| PyTorch | 2022 | 依赖混淆 | 窃取 ssh/gitconfig |

| Codecov | 2021 | 分发入侵 | 潜伏 2 个多月 |

| 依赖混淆(Birsan) | 2021 | 依赖混淆 | 赏金 13 万美元 |

SBOM 深入:CycloneDX vs SPDX

SBOM(Software Bill of Materials,软件物料清单)就像食品的配料表:把你软件里用到的每一个组件、版本、许可证、来源都列清楚。出了漏洞(比如又一个 Log4Shell),你能几秒钟查出"我到底有没有用、用在哪",而不是全公司人肉排查一整周。

目前有两大主流标准:

| 维度 | CycloneDX | SPDX |

| --- | --- | --- |

| 主导组织 | OWASP | Linux 基金会 |

| 定位 | 偏安全,漏洞/VEX 友好 | 偏许可证合规,历史悠久 |

| 格式 | JSON、XML | JSON、YAML、RDF、tag-value |

| 标准 | 事实标准 | ISO/IEC 5962 国际标准 |

| 强项 | 依赖关系、漏洞关联 | 许可证追踪、法律合规 |

| 适合 | 安全团队、DevSecOps | 法务、开源合规审查 |

简单选型:主要为了安全和漏洞追溯,选 CycloneDX;主要为了许可证合规、或对接需要 ISO 标准的甲方,选 SPDX;很多工具其实两种都能导出。

生成 SBOM(CycloneDX 版):

bashCode
# 为 npm 项目生成 CycloneDX 格式 SBOM
npx @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json

# 指定输出格式为 XML
npx @cyclonedx/cyclonedx-npm --output-format xml --output-file sbom.cdx.xml

# 只包含生产依赖,排除 devDependencies
npx @cyclonedx/cyclonedx-npm --omit dev --output-file sbom-prod.json

# 用通用工具 Syft 生成(支持容器镜像/文件系统)
syft packages dir:. -o cyclonedx-json=sbom.json
syft packages my-image:latest -o spdx-json=sbom.spdx.json

消费 SBOM(拿它做漏洞扫描):

bashCode
# 用 Grype 直接扫 SBOM,找出其中的已知漏洞
grype sbom:./sbom.cdx.json

# 用 OSV-Scanner 扫描 SBOM
osv-scanner --sbom=sbom.cdx.json

# 在 CI 里对 SBOM 做许可证/组件策略校验
cyclonedx-cli analyze --input-file sbom.cdx.json

实践建议:把 SBOM 生成挂进 CI,每次发布都产出并归档一份带版本号的 SBOM;同时把 SBOM 当作制品一并签名,做到"发了什么、成分是什么"可追溯、可审计。

语义化版本与 lock 文件机制

语义化版本(semver) 用 `主版本.次版本.修订号`(如 `4.17.21`)三段来表达兼容性承诺:主版本变了意味着有破坏性改动,次版本是新增功能且向后兼容,修订号是修 bug。npm 的版本范围符号决定了"允许自动升到哪":

| 写法 | 含义 | 允许升级范围 | 风险 |

| --- | --- | --- | --- |

| `4.17.21` | 精确锁定 | 只此一个版本 | 最安全,但要手动升 |

| `~4.17.21` | 允许修订号升 | 4.17.21 到 <4.18.0 | 较低,只收 bug 修复 |

| `^4.17.21` | 允许次版本升 | 4.17.21 到 <5.0.0 | 中,可能引入新代码 |

| `*` / `latest` | 任意版本 | 全部 | 高,随时可能破坏 |

关键取舍:`^`(npm 默认)方便自动收安全补丁,但也意味着 `npm install` 在不同时间可能装到不同版本——这正是 ua-parser-js 那种"新版本投毒"能瞬间扩散的原因。所以真正保证"每次装的都一样"的,不是 `package.json` 里的范围,而是 lock 文件

lock 文件 记录了整棵依赖树里每个包被解析出的确切版本和完整性哈希(`integrity`)。它的作用有三:可复现构建(团队和 CI 装到完全相同的版本)、防篡改(哈希对不上就报错)、可审计(清楚知道实锁了什么)。

bashCode
# 严格按 lock 文件安装,版本对不上直接报错(CI 必用)
npm ci

# 对比:npm install 会在允许范围内更新 lock 文件
npm install

# 查某个包为什么被装进来(追溯传递依赖)
npm ls lodash

# 只更新某个包到允许范围内最新,并刷新 lock
npm update lodash

黄金法则:lock 文件必须提交进版本库;CI 一律用 `npm ci` 而非 `npm install`;生产核心依赖倾向精确或 `~`,配合 Renovate/Dependabot 做"可控的自动升级"。

npm / pnpm / yarn 安全特性对比

三大包管理器在安全设计上各有侧重,选型时值得了解。

| 特性 | npm | pnpm | yarn(Berry) |

| --- | --- | --- | --- |

| 依赖存储 | 扁平化 node_modules | 硬链接 + 内容寻址 | PnP 或 node_modules |

| 幽灵依赖 | 容易出现 | 严格隔离,默认杜绝 | PnP 下杜绝 |

| lock 文件 | package-lock.json | pnpm-lock.yaml | yarn.lock |

| provenance | 支持 npm provenance | 支持 | 部分支持 |

| 安装脚本控制 | ignore-scripts | 默认更保守,可白名单 | enableScripts 配置 |

| 观察期机制 | 无 | minimumReleaseAge | 无原生 |

| 完整性校验 | integrity 哈希 | 强内容寻址校验 | 校验 + PnP 锁 |

几个重点:

pnpm 硬链接隔离:pnpm 把所有包存在全局 store,项目里用硬链接引用,且只把你显式声明的依赖放进可见路径,天然杜绝"幽灵依赖"(用了没在 package.json 声明的包)。这不仅省磁盘,也缩小了攻击面。
yarn PnP(Plug'n'Play):干脆不生成 `node_modules`,用一个映射文件精确控制每个包能访问谁,依赖边界更严格。
npm provenance / attestation:发布时用 `--provenance` 生成可验证的来源证明(记录是哪条 CI 流水线、哪个 commit 构建的),下游可校验包确实来自声称的仓库,对抗账号劫持式投毒。
bashCode
# npm 发布时附带 provenance 溯源(需在受支持的 CI 环境如 GitHub Actions)
npm publish --provenance --access public

pnpm 的 minimumReleaseAge(观察期) 是对抗"新版本投毒"的利器——要求包版本发布满一定时间才允许被安装,把 ua-parser-js 那种"发布几小时内就被发现下架"的恶意版本挡在门外:

yamlCode
# .pnpmfile 或 pnpm-workspace.yaml / package.json 中配置
# 要求依赖版本至少发布 1440 分钟(1 天)后才可安装
minimumReleaseAge: 1440
# 对个别可信包可豁免
minimumReleaseAgeExclude:
  - "@mycompany/*"

完整的 GitHub Actions 安全流水线

把前面讲的检测能力串成一条流水线,让每次 PR 都自动过一遍安全关卡:依赖审查 + 漏洞审计 + 代码扫描 + 开源漏洞库比对。

yamlCode
# .github/workflows/supply-chain.yml
name: Supply Chain Security
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  # 1) 对 PR 新增依赖做许可证与漏洞审查
  dependency-review:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high
          deny-licenses: GPL-3.0, AGPL-3.0

  # 2) npm audit 门禁
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm audit --audit-level=high

  # 3) OSV-Scanner 扫描(Google 的开源漏洞库)
  osv-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google/osv-scanner-action@v1
        with:
          scan-args: "--lockfile=package-lock.json"

  # 4) CodeQL 代码级安全分析
  codeql:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript-typescript
      - uses: github/codeql-action/analyze@v3

Renovate 配置(比 Dependabot 更灵活的自动更新):

jsonCode
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "schedule": ["before 9am on monday"],
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    },
    {
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "labels": ["breaking-change"]
    }
  ],
  "vulnerabilityAlerts": {
    "labels": ["security"],
    "automerge": true
  },
  "minimumReleaseAge": "3 days"
}

Snyk CLI / OSV-Scanner / socket 本地扫描:

bashCode
# Snyk:测试项目、监控、并尝试修复
snyk test --severity-threshold=high
snyk monitor
snyk fix

# OSV-Scanner:基于开源漏洞库离线/本地扫描
osv-scanner --lockfile=package-lock.json
osv-scanner scan -r .   # 递归扫整个目录

# Socket:侧重恶意行为(是否偷偷联网、读文件、混淆代码)
npx @socketsecurity/cli scan ./

.npmrc 加固与私有 registry

`.npmrc` 安全加固——把风险默认关掉:

bashCode
# .npmrc
# 禁止所有安装脚本自动执行(防 postinstall 投毒)
ignore-scripts=true
# audit 门槛,低于 high 不报错
audit-level=high
# 强制使用官方或私有 registry,避免被劫持到未知源
registry=https://registry.npmjs.org/
# 私有 scope 走内部 registry,防依赖混淆
@mycompany:registry=https://nexus.internal.com/repository/npm/
# 安装时校验 engines,避免不兼容运行时
engine-strict=true
# 只用 lock 文件,禁止隐式修改
save-exact=true

私有 registry(Verdaccio / Nexus) 让内部包有独立可信来源,并可对公共包做统一代理、缓存与审计,是对抗依赖混淆的关键一环:

yamlCode
# Verdaccio config.yaml(轻量级私有 registry)
storage: ./storage
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
packages:
  '@mycompany/*':
    # 内部包只允许认证用户发布,且不回退到公共源
    access: $authenticated
    publish: $authenticated
    proxy: # 留空,绝不从公共源解析同名包,杜绝依赖混淆
  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs
bashCode
# 启动 Verdaccio 并把项目指向它
npx verdaccio
npm set registry http://localhost:4873/
# 发布内部包到私有 registry
npm publish --registry http://localhost:4873/

用 overrides / resolutions 修复传递依赖——当漏洞藏在你无法直接控制的深层依赖里时,强制全树使用安全版本:

jsonCode
{
  "overrides": {
    "ua-parser-js": "1.0.33",
    "minimist": ">=1.2.6",
    "ansi-regex@>2.1.1 <5.0.1": "5.0.1"
  },
  "resolutions": {
    "**/lodash": "4.17.21"
  }
}

其中 `overrides` 是 npm/pnpm 的写法,`resolutions` 是 yarn 的写法,效果类似:不管哪一层依赖引用了这个包,都被强制拉到指定的安全版本。

许可证合规扫描

依赖不只有安全风险,还有法律风险。误用 GPL/AGPL 这类"传染性"许可证,可能被要求开源你自己的商业代码。所以合规扫描应和安全扫描并列进 CI。

bashCode
# 用 license-checker 列出所有依赖的许可证
npx license-checker --summary

# 只输出生产依赖,且发现禁用许可证就失败
npx license-checker --production --failOn "GPL-3.0;AGPL-3.0"

# 导出为 CSV 供法务审阅
npx license-checker --csv --out licenses.csv

常见许可证风险分级:MIT / ISC / BSD / Apache-2.0 属于宽松许可,商用基本无忧;LGPL 有条件可用(动态链接较安全);GPL / AGPL 具传染性,闭源商业项目须格外谨慎,AGPL 连"通过网络提供服务"都算触发。

依赖健康度评估:OpenSSF Scorecard

选包时除了看功能,还要看它"健不健康、值不值得托付"。OpenSSF Scorecard 把这件事量化成 0-10 分,从多个维度自动打分:

| 维度 | 考察什么 |

| --- | --- |

| Maintained | 是否还在活跃维护、近期有无提交 |

| Code-Review | 合并是否经过代码评审 |

| Branch-Protection | 主分支是否开启保护规则 |

| Dangerous-Workflow | CI 里有无危险写法(如 pull_request_target 滥用) |

| Token-Permissions | CI token 是否遵循最小权限 |

| Signed-Releases | 发布是否签名 |

| Vulnerabilities | 是否有未修复的已知漏洞 |

| Dependency-Update-Tool | 是否用了 Dependabot/Renovate |

bashCode
# 对某个开源仓库跑 Scorecard 评分
scorecard --repo=github.com/expressjs/express

# 在 CI 里生成并上传结果
scorecard --repo=github.com/myorg/myrepo --format=json > scorecard.json

综合评估维度:下载量与趋势、维护活跃度(最近提交/issue 响应)、Scorecard 分数、有无安全策略(SECURITY.md)、维护者数量(单点维护是风险,xz 就是教训)、传递依赖规模(越少越好)。

漏洞优先级:CVSS + EPSS + 可达性

漏洞报告一多,全修修不过来,必须排优先级。只看 CVSS 分数是不够的——它衡量"理论严重性",不代表"实际会不会被打、以及对你有没有影响"。成熟做法是三者结合:

| 指标 | 回答的问题 | 特点 |

| --- | --- | --- |

| CVSS | 漏洞有多严重 | 0-10 静态评分,偏理论 |

| EPSS | 未来 30 天被利用的概率 | 0-100% 动态,基于真实威胁情报 |

| 可达性(reachability) | 你的代码真的调到它了吗 | 结合调用图分析,最贴合实际 |

举例:一个 CVSS 9.8 的漏洞,如果 EPSS 只有 1%、且经可达性分析发现漏洞函数在你项目里从未被调用,那它的实际优先级可能低于一个 CVSS 6.5 但 EPSS 高达 80%、又恰好在你热路径上的漏洞。

bashCode
# 一些 SCA 工具已支持可达性分析,只报"真正调用到的"漏洞
snyk test --severity-threshold=high --reachable
# 查询某 CVE 的 EPSS 分数(FIRST 提供公开 API)
curl "https://api.first.org/data/v1/epss?cve=CVE-2021-44228"

优先级公式可粗略理解为:真实风险 ≈ 严重性(CVSS) × 被利用概率(EPSS) × 可达性 × 资产重要度。把有限的修复精力投给"又严重、又可能被打、又真的用到、又在核心系统"的那一小撮。

事故响应流程

万一真中招了(比如收到 ua-parser-js 那样的公告),慌乱无济于事,按流程走:

1. 发现(Detect):通过监控告警、安全公告、SCA 扫描或社区消息得知。第一时间确认信息真伪与影响版本范围。

2. 评估(Assess):用 `npm ls ` 确认自己是否受影响、哪些服务用到、是直接还是传递依赖、恶意版本是否已进过生产。对照 SBOM 快速定位。

3. 遏制与修复(Contain & Fix):立即锁定或降级到安全版本(`overrides` 强制)、重装并清 `node_modules` 与缓存、轮换所有可能泄露的密钥(token、私钥、云凭证)、必要时下线受影响功能。

4. 复盘(Review):还原时间线、评估实际损失、补齐检测缺口(为什么没早发现)、更新 playbook。把这次的教训固化成新的门禁规则或观察期配置。

bashCode
# 事故排查常用命令
npm ls ua-parser-js              # 我用了吗?在哪层?
npm why ua-parser-js             # 为什么被装进来(pnpm/npm 新版)
rm -rf node_modules package-lock.json && npm cache clean --force
npm ci                           # 干净重装

关键心态:一旦怀疑密钥可能已泄露,就当它已经泄露——立即轮换,别赌"应该没事"。

更多最佳实践清单

生产依赖倾向精确锁定,用自动化工具做可控升级,而非放任 `^` 自由漂移。
CI 一律 `npm ci`,并把 audit / dependency-review / OSV-Scanner 设为合并门禁。
默认 `ignore-scripts=true`,对确需安装脚本的包单独白名单放行。
私有 scope 严格绑定私有 registry,堵死依赖混淆。
引入观察期(pnpm minimumReleaseAge 或 Renovate minimumReleaseAge),躲开"发布即投毒"。
每次发布产出并归档带版本的 SBOM,并对制品做签名与 provenance。
定期跑 Scorecard 评估核心依赖健康度,警惕单点维护的关键库。
许可证扫描与安全扫描并列,避免法律地雷。
用 CVSS + EPSS + 可达性三者定优先级,别被单一分数带偏。
事故 playbook 事先写好、演练过,出事时按流程走而非临场发挥。

总结

依赖安全的核心思想是"你要为你运行的每一行代码负责,哪怕它不是你写的"。做法可归纳为三点:清楚知道自己依赖了什么(SBOM + lock 文件)、持续扫描已知漏洞(audit + CI 门禁 + 监控)、谨慎引入与升级(评审 + 小步稳更)。

| 维度 | 要点 | 推荐做法 |

| --- | --- | --- |

| 可见性 | 知道依赖了什么 | 维护 SBOM,提交 lock 文件 |

| 检测 | 发现已知漏洞 | audit + Snyk + CI 门禁 |

| 更新 | 保持依赖新鲜 | Dependabot/Renovate 小步快跑 |

| 引入 | 防恶意包 | 评审 + 核对包名 + 关注安装脚本 |

| 修复 | 分级响应 | 按 CVSS 与可达性定优先级 |

| 供应链 | 防上游投毒 | 私有 registry + provenance 校验 |

一句话记忆:管住"别人写的代码",才守得住"自己的系统"。