2025 年,开源仓库里冒出了超过 454,000 个恶意包——这不是漏洞数量,是有人故意投毒的包数量。Sonatype 的 2026 年《软件供应链状态报告》把累计数字摆到了桌面上:自 2019 年以来,恶意包总量突破 120 万。绝大多数团队根本没意识到自己装了多少依赖,更不知道这些依赖的上游是否还安全。供应链攻击的爆炸半径正在随依赖深度指数级膨胀,而你每天 pip install 或 npm install 的那一刻,就是攻击者进入你系统的入口。
攻击者怎么往你的依赖里掺毒
供应链攻击不是新概念,但手法在过去两年里快速进化。三种最常见的模式:
同名仿冒(Typosquatting)——攻击者注册一个和流行包名字极其相似的包,等你手滑敲错一个字母就装上了毒。比如 reqeusts 伪装 requests,loddash 伪装 lodash。
依赖混淆(Dependency Confusion)——公司内部有个私有包 mycompany-utils,攻击者在 PyPI 上同名发布一个版本号更高的公开包。构建工具默认选最高版本,于是公开毒包替代了内部安全包。2021 年 Alex Birsan 用这种方法成功渗透了 Apple、Microsoft、PayPal 的内部系统,报告出来后整个行业才真正重视。
维护者劫持(Maintainer Hijack)——攻击者拿到流行包维护者的账号控制权(社工、凭证泄露、甚至直接购买),然后从"合法上游"推送恶意更新。2022 年的 ua-parser-js 事件就是典型:维护者账号被入侵,三个版本被植入挖矿和密码窃取脚本,数百万下载量在几天内中招。
这些手法有一个共同特征:你的代码没问题,你的依赖也没问题——但依赖的来源出了问题。 传统安全扫描盯着你自己写的代码,对上游投毒几乎无感。
数字背后的趋势
Sonatype 报告的几个关键数字值得逐条看:
| 指标 | 数值 |
|---|---|
| 2025 年新增恶意包 | 454,000+ |
| 2019–2025 累计恶意包 | 1,200,000+ |
| 年增长率 | 超过 2 倍(对比 2023→2025) |
增长曲线不是线性的,是指数的。原因很简单:攻击者发现这比直接攻破目标公司的防火墙成本低得多。一个恶意包发布后,所有使用该依赖的组织自动中招,不需要逐个突破。开源生态的便利性——一键安装、自动拉取最新版——恰好成了攻击的放大器。
另一个容易被忽略的事实:恶意包的平均存活时间在缩短。社区和平台的安全响应确实在加快,但攻击者的发布速度更快。2025 年平均每个恶意包在被下架前已经被下载了数百次,足够完成一次数据窃取或后门植入。
实战:给你的项目装上供应链防线
理论讲够了,下面是可以直接跑的防御手段。从最基础的开始,逐步加厚。
第一步:锁版本 + 审计现有依赖
先搞清楚你到底装了什么,然后锁死版本号,不给攻击者"更高版本自动替换"的机会。
# Python 项目:生成完整依赖树(含间接依赖)
pip install pipdeptree
pipdeptree --json-tree > deps-tree.json
# 扫描已知漏洞(包括间接依赖)
pip install pip-audit
pip-audit -r requirements.txt --desc
# npm 项目同理
npm audit --json > npm-audit.json
npx npm-remote-ls your-package # 查看间接依赖树
关键动作: requirements.txt 和 package.json 里必须写死版本号(==1.2.3 而不是 >=1.2.0),并在 CI 里加一步校验锁文件未被篡改:
# CI 中校验 hash
pip-audit -r requirements.lock
sha256sum -c requirements.lock.sha256
第二步:私有镜像 + 依赖混淆阻断
依赖混淆攻击的核心是"公开包版本号更高就自动替代私有包"。阻断方法:在包管理器配置里声明优先从私有仓库拉取,公开仓库只作为白名单内的补充。
# pip.conf — 优先内部仓库,公开 PyPI 只取白名单包
# 文件位置: ~/.pip/pip.conf 或 /etc/pip.conf
[global]
index-url = https://your-internal-mirror.example.com/simple/
extra-index-url = https://pypi.org/simple/
# 更安全的做法:只用内部镜像,完全不连公网 PyPI
# 在 CI 环境中:
[global]
index-url = https://your-internal-mirror.example.com/simple/
# 不设置 extra-index-url,彻底切断外部源
npm 的对应配置:
// .npmrc
registry=https://your-internal-npm.example.com/
// 白名单包才从公网拉取
// 严格模式下直接禁用公网源
第三步:CI 流水线里的供应链门禁
把审计和校验嵌入 CI,每次构建自动检查。下面是一个 GitHub Actions 工作流示例:
name: supply-chain-gate
on:
pull_request:
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 锁定 hash,防止 action 本身被篡改
# 使用 commit SHA 而不是 tag
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install tools
run: pip install pip-audit pipdeptree
- name: Audit dependencies
run: |
# 检查已知漏洞
pip-audit -r requirements.lock --strict
# 生成依赖树供人工复查
pipdeptree > deps-tree.txt
- name: Verify lock file integrity
run: |
# requirements.lock.sha256 需提前生成并提交到仓库
sha256sum -c requirements.lock.sha256
- name: Check for typosquatting
run: |
# 简易同名仿冒检测:对比依赖名与 Top 100 包的编辑距离
python scripts/typosquat_check.py requirements.lock
同名仿冒检测脚本(scripts/typosquat_check.py)可以这样写:
"""简易同名仿冒检测:计算依赖名与流行包名的编辑距离"""
import re
import sys
# 常见流行包列表(实际项目中应从下载量排行动态获取)
POPULAR_PACKAGES = [
"requests", "flask", "django", "numpy", "pandas",
"lodash", "express", "react", "axios", "moment",
]
def edit_distance(a: str, b: str) -> int:
"""Levenshtein 编辑距离"""
if len(a) < len(b):
return edit_distance(b, a)
if len(b) == 0:
return len(a)
prev = range(len(b) + 1)
for i, ca in enumerate(a):
curr = [i + 1]
for j, cb in enumerate(b):
curr.append(min(
prev[j + 1] + 1, # 删除
curr[j] + 1, # 插入
prev[j] + (ca != cb), # 替换
))
prev = curr
return prev[-1]
def check_lock_file(path: str, threshold: int = 2):
"""扫描 lock 文件中编辑距离过近的包名"""
with open(path) as f:
deps = [line.split("==")[0].strip() for line in f if "==" in line]
warnings = []
for dep in deps:
for popular in POPULAR_PACKAGES:
dist = edit_distance(dep.lower(), popular.lower())
if 0 < dist <= threshold and dep.lower() != popular.lower():
warnings.append(
f"⚠ {dep} 与流行包 {popular} 编辑距离仅 {dist},疑似仿冒"
)
for w in warnings:
print(w)
if warnings:
sys.exit(1)
print("✅ 未发现疑似同名仿冒包")
if __name__ == "__main__":
check_lock_file(sys.argv[1])
第四步:SBOM 生成与签名验证
软件物料清单(SBOM)让你随时知道"这个发布版本里到底包含了哪些上游组件"。Sigstore 提供免费的签名和验证基础设施,不需要自己管理密钥。
# 生成 SBOM(Syft 是目前最成熟的工具)
# 安装
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# 对容器镜像或目录生成 SBOM
syft dir:./ -o spdx-json > sbom.spdx.json
# 用 cosign 对制品签名(Sigstore 无密钥模式)
# 安装 cosign
curl -sSfL https://raw.githubusercontent.com/sigstore/cosign/main/install.sh | sh -s -- -b /usr/local/bin
# 签名(Keyless 模式,用你的 OIDC 身份)
cosign sign blob sbom.spdx.json
# 验证别人发布的制品签名
cosign verify blob sbom.spdx.json \
--certificate sbom.spdx.json.pem \
--signature sbom.spdx.json.sig \
--certificate-identity developer@example.com \
--certificate-oidc-issuer https://accounts.google.com
采纳路径与取舍
供应链安全没有银弹,但有一条清晰的优先级路径:
- 先锁版本、先审计——这是成本最低、收益最高的第一步。不锁版本,后面所有防线都有被绕过的风险。
- 切私有镜像——如果你有内部包,这一步必须做。依赖混淆攻击不需要任何技术突破,只需要版本号更高。
- CI 门禁自动化——人工审计不可能跟上 120 万恶意包的规模,必须让 CI 替你盯着。
- SBOM + 签名——这是长期基建,适合已经成熟的项目和组织。前期投入大,但在合规和溯源场景价值极高。
取舍提醒:
- 完全切断公网源意味着你需要自己维护镜像的同步和更新,私有镜像落后于上游会拖慢开发。
- 锁版本会阻止自动获取安全补丁版本,必须建立定期升级流程(建议每月一次集中升级 + 审计)。
- SBOM 生成会增加构建时间约 10–30 秒,CI 资源消耗也会上升。
- typosquat 检测的阈值设太低会误报,设太高会漏报,需要根据团队实际依赖列表调校。
供应链安全的核心认知转变是:你不仅要信任自己的代码,还要持续验证你借来的每一行代码的来源。 120 万恶意包的数字不会停止增长,但你的防线可以从今天开始。