依赖安全与供应链

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.01.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 忽略
修复引入新 bugaudit fix 升级了破坏性变更在 CI 中检查,手动逐步修复
私有包安全内部包名可能被外部注册作用域 registry + @company/ 前缀
Lockfile 冲突多人修改依赖频繁合并主分支,避免长期分支

最佳实践

  1. 冻结 lockfile:CI 用 --frozen-lockfile,确保安装结果一致。
  2. 定期 audit:每周运行 npm audit,CI 中强制检查。
  3. 精确版本save-exact=true,避免版本范围。
  4. 禁用 scriptsignore-scripts=true,手动运行必要的脚本。
  5. 最小依赖:能用原生 API 实现的功能不引入包。
  6. 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 包的 postinstallpreinstall 等脚本在 npm install 时自动执行,拥有与当前用户相同的权限。风险:(1) 恶意包可以在安装时执行任意命令(如窃取环境变量、植入后门);(2) 即使是合法包的脚本也可能被劫持。防护:(1) pnpm install --ignore-scripts 禁用所有安装脚本;(2) .npmrcignore-scripts=true 永久禁用;(3) 必要的脚本手动执行(如 npx husky install);(4) 使用 npm config set ignore-scripts true 全局禁用。


6. 如何最小化项目的依赖攻击面?

:六个策略:(1) 减少依赖数量——能用原生 API(如 fetchcrypto.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 排除开发依赖的漏洞报告。


相关链接