依赖包安全管理
依赖包安全管理
现代前端项目就像用乐高搭房子:真正自己写的代码往往只占一小部分,剩下的绝大多数由 npm 上的第三方"积木"拼成。一个中等规模的项目 `node_modules` 里动辄有上千个包,其中很多是你根本没直接引用、却被间接拖进来的"依赖的依赖"。只要其中任何一块积木存在漏洞或被人做了手脚,整栋房子都可能塌方。这就是"软件供应链安全"要解决的问题。
据 Sonatype 的年度报告,开源组件的下载量每年以数倍速度增长,针对开源供应链的攻击也随之激增。一个应用 80% 以上 的代码来自第三方依赖,因此依赖安全早已不是"锦上添花",而是"守住底线"。
依赖包安全风险
常见漏洞类型:
风险来源:
影响范围: 应用被入侵、用户数据泄露、服务不可用、企业声誉与合规风险。
为什么依赖安全重要
你可以把自己的代码写得滴水不漏,但只要 `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):
自动更新:
静态分析与监控:
代码示例
示例 1:命令行审计与修复
# 扫描当前项目依赖的已知漏洞
npm audit
# 只看高危及以上
npm audit --audit-level=high
# 自动修复(在语义化版本允许范围内升级)
npm audit fix
# 强制修复(可能引入破坏性大版本,需回归测试)
npm audit fix --force
# 以 JSON 输出,便于脚本处理
npm audit --json > audit-report.json示例 2:在 CI 中设置安全门禁
# .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 强制锁定间接依赖的安全版本
{
"name": "my-app",
"overrides": {
"lodash": "4.17.21",
"semver": ">=7.5.2",
"postcss@<8.4.31": "8.4.31"
}
}示例 4:Dependabot 配置
# .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:安装前的最小化安全检查(防投毒)
# 查看包的元信息,警惕新发布、下载量极低、仿冒名
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依赖管理最佳实践
依赖选择:
版本管理:
依赖锁定:
安全审计:
漏洞修复策略
| 严重程度 | 响应时限 | 处理方式 |
| --- | --- | --- |
| 紧急/严重(Critical) | 立即(小时级) | 马上升级或临时替换、下线受影响功能 |
| 高危(High) | 数天内 | 制定修复计划、测试后尽快部署 |
| 中危(Medium) | 常规迭代 | 纳入下个版本一并升级 |
| 低危(Low) | 择机 | 评估实际暴露面后适时处理 |
修复时优先看漏洞是否"实际可达"——有些漏洞存在于从未被调用的代码路径,实际风险较低,可结合 `npm audit` 的 reachability 信息与业务上下文判断优先级。
供应链安全
来源验证: 使用官方 registry;对内部包用私有 registry;开启 `npm` 的 provenance(发布溯源)校验。
构建环境安全: 隔离构建环境、固定基础镜像、扫描构建产物、CI 中最小化权限与 Secret 暴露。
依赖审查流程: 新引入依赖需评审(安全、许可证、维护度);建立依赖审批清单;关注 `postinstall` 等安装脚本。
常见坑
数据与对比
| 工具 | 类型 | 检测传递依赖 | 自动修复 | 适用场景 |
| --- | --- | --- | --- | --- |
| npm audit | 内置 SCA | 支持 | 有限 | 快速起步 |
| Snyk | 商业 SCA | 支持 | 强,出 PR | 企业级持续监控 |
| Dependabot | 自动更新 | 支持 | 出升级 PR | GitHub 项目 |
| OWASP Dep-Check | 开源 SCA | 支持 | 无 | 自建 CI 流水线 |
| Socket.dev | 行为检测 | 支持 | 无 | 防恶意包/投毒 |
漏洞数据库与资源
软件供应链攻击分类学
要防住供应链攻击,先得看懂攻击者有哪些"进攻套路"。下面把常见的攻击手法逐类拆开,讲清"它是什么、怎么得手、怎么防"。
1. Typosquatting(仿冒包名)
攻击者注册一个和热门包极其相似的名字,赌你手滑打错。比如把 `lodash` 注册成 `lodahs`、`loadsh`,把 `crossenv` 冒充 `cross-env`。2017 年 npm 就下架过一批仿冒包,其中 `crossenv` 会在安装时把你的环境变量(含各种密钥)打包上传到攻击者服务器。防御靠:安装前逐字核对包名、用 `npm view
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 版):
# 为 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(拿它做漏洞扫描):
# 用 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 装到完全相同的版本)、防篡改(哈希对不上就报错)、可审计(清楚知道实锁了什么)。
# 严格按 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 锁 |
几个重点:
# npm 发布时附带 provenance 溯源(需在受支持的 CI 环境如 GitHub Actions)
npm publish --provenance --access publicpnpm 的 minimumReleaseAge(观察期) 是对抗"新版本投毒"的利器——要求包版本发布满一定时间才允许被安装,把 ua-parser-js 那种"发布几小时内就被发现下架"的恶意版本挡在门外:
# .pnpmfile 或 pnpm-workspace.yaml / package.json 中配置
# 要求依赖版本至少发布 1440 分钟(1 天)后才可安装
minimumReleaseAge: 1440
# 对个别可信包可豁免
minimumReleaseAgeExclude:
- "@mycompany/*"完整的 GitHub Actions 安全流水线
把前面讲的检测能力串成一条流水线,让每次 PR 都自动过一遍安全关卡:依赖审查 + 漏洞审计 + 代码扫描 + 开源漏洞库比对。
# .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@v3Renovate 配置(比 Dependabot 更灵活的自动更新):
{
"$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 本地扫描:
# 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` 安全加固——把风险默认关掉:
# .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) 让内部包有独立可信来源,并可对公共包做统一代理、缓存与审计,是对抗依赖混淆的关键一环:
# 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# 启动 Verdaccio 并把项目指向它
npx verdaccio
npm set registry http://localhost:4873/
# 发布内部包到私有 registry
npm publish --registry http://localhost:4873/用 overrides / resolutions 修复传递依赖——当漏洞藏在你无法直接控制的深层依赖里时,强制全树使用安全版本:
{
"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。
# 用 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 |
# 对某个开源仓库跑 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%、又恰好在你热路径上的漏洞。
# 一些 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
3. 遏制与修复(Contain & Fix):立即锁定或降级到安全版本(`overrides` 强制)、重装并清 `node_modules` 与缓存、轮换所有可能泄露的密钥(token、私钥、云凭证)、必要时下线受影响功能。
4. 复盘(Review):还原时间线、评估实际损失、补齐检测缺口(为什么没早发现)、更新 playbook。把这次的教训固化成新的门禁规则或观察期配置。
# 事故排查常用命令
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 # 干净重装关键心态:一旦怀疑密钥可能已泄露,就当它已经泄露——立即轮换,别赌"应该没事"。
更多最佳实践清单
总结
依赖安全的核心思想是"你要为你运行的每一行代码负责,哪怕它不是你写的"。做法可归纳为三点:清楚知道自己依赖了什么(SBOM + lock 文件)、持续扫描已知漏洞(audit + CI 门禁 + 监控)、谨慎引入与升级(评审 + 小步稳更)。
| 维度 | 要点 | 推荐做法 |
| --- | --- | --- |
| 可见性 | 知道依赖了什么 | 维护 SBOM,提交 lock 文件 |
| 检测 | 发现已知漏洞 | audit + Snyk + CI 门禁 |
| 更新 | 保持依赖新鲜 | Dependabot/Renovate 小步快跑 |
| 引入 | 防恶意包 | 评审 + 核对包名 + 关注安装脚本 |
| 修复 | 分级响应 | 按 CVSS 与可达性定优先级 |
| 供应链 | 防上游投毒 | 私有 registry + provenance 校验 |
一句话记忆:管住"别人写的代码",才守得住"自己的系统"。