依赖安全与供应链
What — 什么是依赖安全
依赖安全是指确保项目使用的第三方包(npm 依赖)不包含恶意代码、已知漏洞或被篡改。供应链攻击是指攻击者通过侵入上游依赖包来影响所有下游用户。
威胁类型
| 威胁 | 说明 | 案例 |
|---|---|---|
| 已知漏洞 | 依赖包存在安全缺陷 | lodash 原型污染 |
| 恶意包 | 攻击者发布与知名包同名的恶意包 | crossenv(冒充 cross-env) |
| 包劫持 | 攻击者获取维护者账号控制包 | event-stream 事件 |
| Typosquatting | 包名与知名包相似,利用拼写错误 | npm 官方包 + 后缀 |
| 依赖混淆 | 内部私有包名被攻击者在 npm 上注册同名包 | Alex Birsan 研究 |
| Install Scripts | 包在安装时执行恶意脚本 | postinstall 钩子 |
Why — 为什么依赖安全重要
1. 现代前端项目依赖数量庞大
一个典型的 React 项目有 1000+ 个传递依赖,每个都是潜在的攻击面。
2. 供应链攻击增长迅速
npm 是最大的包管理生态,攻击者越来越倾向于攻击上游依赖而非直接攻击目标。
3. 影响范围大
一个恶意包一旦进入供应链,所有安装它的项目都会受影响,可能影响数百万用户。
How — 怎么防护
1. npm audit
# 检查已知漏洞
npm audit
# 自动修复
npm audit fix
# 强制修复(可能有破坏性变更)
npm audit fix --force
# 查看详情
npm audit --json
pnpm 的安全审计:
pnpm audit
pnpm audit --fix
2. 锁文件(Lockfile)
# 始终提交 lockfile
git add pnpm-lock.yaml
# 安装时使用精确版本
pnpm install --frozen-lockfile # CI 中使用,禁止更新 lockfile
为什么锁文件重要:package.json 中的版本范围(如 ^1.2.0)允许安装 1.2.0 到 1.9.9 的任何版本。如果 1.2.0 安全但 1.2.1 被投毒,没有锁文件就可能安装到恶意版本。
3. 精确版本控制
// ❌ 宽松版本范围
{
"dependencies": {
"lodash": "^4.17.0"
}
}
// ✅ 精确版本
{
"dependencies": {
"lodash": "4.17.21"
}
}
# pnpm 配置:默认使用精确版本
pnpm config set save-exact true
# 或者使用 npm
npm config set save-exact true
4. 禁用 Install Scripts
# 安装时忽略所有 postinstall 等脚本
pnpm install --ignore-scripts
# 在 .npmrc 中永久配置
echo "ignore-scripts=true" >> .npmrc
// package.json 中声明需要运行的脚本
{
"scripts": {
"prepare": "husky install" // 只允许白名单脚本
}
}
5. 依赖审查工具
# npq — 安装前检查包的安全性
npx npq install lodash
# npm-check — 检查过时的依赖
npx npm-check
# socket — 实时监控依赖安全
npx socket
# Snyk — 企业级安全扫描
npx snyk test
npx snyk monitor
GitHub Dependabot:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
reviewers:
- 'security-team'
6. 依赖混淆防护
// .npmrc — 确保私有包从私有 registry 获取
@my-company:registry=https://npm.my-company.com/
# pnpm 配置作用域 registry
pnpm config set @my-company:registry https://npm.my-company.com/
7. SBOM — 软件物料清单
# 生成 SBOM
npx @cyclonedx/cyclonedx-npm --output-format json > sbom.json
# 或使用 Syft
syft dir:./ -o cyclonedx-json > sbom.json
8. CI 安全检查
# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install --frozen-lockfile
- run: pnpm audit --audit-level moderate
- name: Check licenses
run: npx license-checker --failOn 'GPL-3.0'
9. 包发布前检查
# 检查包内容
npx npm-packlist
# 检查包体积
npx bundlephobia lodash@4.17.21
# 检查包是否被篡改
npm view lodash integrity
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| audit 误报 | 间接依赖的漏洞不影响实际使用 | 用 overrides 替换或 npm audit 忽略 |
| 修复引入新 bug | audit fix 升级了破坏性变更 | 在 CI 中检查,手动逐步修复 |
| 私有包安全 | 内部包名可能被外部注册 | 作用域 registry + @company/ 前缀 |
| Lockfile 冲突 | 多人修改依赖 | 频繁合并主分支,避免长期分支 |
最佳实践
- 冻结 lockfile:CI 用
--frozen-lockfile,确保安装结果一致。 - 定期 audit:每周运行
npm audit,CI 中强制检查。 - 精确版本:
save-exact=true,避免版本范围。 - 禁用 scripts:
ignore-scripts=true,手动运行必要的脚本。 - 最小依赖:能用原生 API 实现的功能不引入包。
- Dependabot:自动化依赖更新和安全修复。
面试题
1. 什么是幽灵依赖?pnpm 如何解决?
答:幽灵依赖是指代码中 import 了一个未在 package.json 中声明的包——因为它被某个直接依赖间接安装了。npm/yarn 的扁平化 node_modules 让所有包都能被访问到。问题:间接依赖升级或移除时,你的代码会突然报错。pnpm 的非扁平化结构(符号链接 + .pnpm 目录)只允许访问 package.json 中声明的依赖,import 未声明的包会报 Module Not Found,从机制层面消除幽灵依赖。
2. 依赖混淆攻击是什么?如何防护?
答:攻击者在公共 npm registry 上注册与公司内部私有包同名的包。当 npm 安装时,如果私有 registry 不可用或配置错误,npm 会回退到公共 registry 下载攻击者的恶意包。防护:(1) 作用域 registry——私有包使用 @company/ 作用域,配置 .npmrc 中该作用域指向私有 registry;(2) registry 回退禁用——registry=https://npm.company.com/ 配置严格的 registry,不允许回退到 npmjs.org;(3) lockfile 锁定——确保安装的是已知安全版本;(4) 预安装检查——CI 中验证所有 @company/ 包都来自私有 registry。
3. npm audit 的工作原理是什么?有什么局限?
答:npm audit 将项目的依赖树发送到 npm 的安全数据库,比对已知漏洞列表(CVE),返回受影响的包和修复建议。局限:(1) 只检测已知漏洞——新漏洞或未公开的漏洞无法检测;(2) 可能误报——间接依赖的漏洞可能在实际使用中不触发;(3) 修复可能破坏——audit fix 升级依赖可能引入不兼容变更;(4) 不检测恶意包——只能检测已知漏洞编号,不能检测恶意代码;(5) 依赖 npm 数据库——需要联网,且数据库可能不完整。
4. 为什么要在 CI 中使用 --frozen-lockfile?
答:--frozen-lockfile 要求安装完全匹配 lockfile,如果 lockfile 与 package.json 不一致则报错退出。原因:(1) 确定性——确保 CI 安装的依赖版本与本地开发完全一致,排除”本地能跑 CI 跑不了”的问题;(2) 安全性——防止 CI 中意外安装新版本(可能包含恶意代码);(3) 速度——跳过版本解析和 lockfile 生成,安装更快;(4) 审计——如果依赖变化了,必须通过 PR 更新 lockfile,便于 Code Review。
5. Install Scripts 有什么安全风险?如何防护?
答:npm 包的 postinstall、preinstall 等脚本在 npm install 时自动执行,拥有与当前用户相同的权限。风险:(1) 恶意包可以在安装时执行任意命令(如窃取环境变量、植入后门);(2) 即使是合法包的脚本也可能被劫持。防护:(1) pnpm install --ignore-scripts 禁用所有安装脚本;(2) .npmrc 中 ignore-scripts=true 永久禁用;(3) 必要的脚本手动执行(如 npx husky install);(4) 使用 npm config set ignore-scripts true 全局禁用。
6. 如何最小化项目的依赖攻击面?
答:六个策略:(1) 减少依赖数量——能用原生 API(如 fetch、crypto.subtle)实现的不引入包;(2) 选择维护良好的包——检查 GitHub star、issue 响应速度、最近提交时间;(3) 精确版本——save-exact=true,避免版本范围带来的不确定性;(4) 锁文件提交——确保所有环境安装完全相同的版本;(5) 定期更新——Dependabot 自动提交安全更新 PR;(6) 依赖审计——CI 中 npm audit --audit-level high 强制检查。
7. 什么是 SBOM?为什么它对供应链安全重要?
答:SBOM(Software Bill of Materials)是软件的”配料表”——列出项目所有直接和间接依赖的名称、版本、来源。它对供应链安全重要因为:(1) 快速响应——当某个包爆出漏洞(如 log4j),你可以立即通过 SBOM 查出是否使用了受影响版本;(2) 合规要求——美国行政令要求软件供应商提供 SBOM;(3) 透明度——让安全团队了解完整的依赖图,而非只看 package.json。生成工具:@cyclonedx/cyclonedx-npm、Syft。
8. 如何处理 npm audit 报告的漏洞但不影响项目稳定性?
答:四步处理流程:(1) 评估影响——检查漏洞的实际风险,CVSS 评分低或项目中未使用受影响功能的可以暂时忽略;(2) 优先级排序——先修复 Critical/High 级别的漏洞;(3) 渐进修复——npm audit fix 先尝试自动修复,如果引入不兼容变更,用 overrides(npm)或 pnpm.overrides 替换特定子依赖的安全版本,而非升级直接依赖;(4) 接受风险——无法立即修复的漏洞,在 package.json 中用 // 注释说明原因和计划修复时间,或使用 npm audit 的 --omit 排除开发依赖的漏洞报告。