软件供应链攻击的重心正在发生变化。相比入侵大型软件厂商并篡改更新渠道,攻击者越来越多地盯上 npm、PyPI、Docker Hub、GitHub Actions 和开发者工具:接管维护者账号、投放恶意版本、利用安装脚本窃取凭据,再借助被盗令牌继续污染其他项目。
来源材料指出,2025 年至 2026 年上半年出现了一批规模显著扩大的开源供应链攻击,部分行动甚至使用蠕虫式传播和迭代入侵。OpenSSF 汇总的数据也显示,2025 年检测到的恶意开源软件包数量较 2024 年增长了 1,444%。面对这种攻击,单独部署一个漏洞扫描器远远不够,防线必须覆盖依赖进入、流水线执行、凭据使用、制品发布和事件响应的完整链路。
为什么开源仓库成了高收益入口
传统供应链攻击通常需要长期潜伏、掌握厂商构建系统,并精确操纵签名或更新基础设施。开源生态降低了这些门槛:一个被社会工程攻陷的维护者账号、一条权限过大的 GitHub Actions 工作流,或者一个无人维护但下载量可观的软件包,都可能成为入口。
这种攻击同时具备三个特点:
- 传播效率高:热门包可能被成千上万个直接和间接依赖引用,恶意版本一旦进入自动更新流程,影响会迅速扩散。
- 容易接触高价值凭据:安装过程经常运行在开发者电脑或 CI Runner 上,环境中可能存在云密钥、npm 发布令牌、GitHub Token 和代码签名材料。
- 暴露后较为嘈杂:恶意包通常会被社区快速分析并下架,但几小时的窗口对高下载量依赖已经足够危险。
AI 编程工具又放大了这个问题。编码代理可以快速搜索和引入依赖,却未必能判断一个名称相似、刚刚发布或携带恶意 MCP 工具定义的软件包是否可信。攻击者同样可以利用 AI 批量生成仿冒包、钓鱼材料和针对不同生态的载荷。因此,AI 生成的依赖变更也必须经过与人工提交相同的审查和策略检查。
把依赖入口改成“内部仓库加冷却窗口”
有效的第一道门不是“发现恶意代码后报警”,而是让未经验证的新版本暂时无法进入构建环境。团队可以把公网仓库同步到内部制品库,在同步过程中执行恶意软件检测、许可证检查、漏洞扫描和发布时间冷却。
Node.js 项目可以这样实践。下面的 .npmrc 会锁定内部仓库、关闭生命周期脚本,并要求安装严格遵循锁文件。运行前请将 registry.example.com 替换为组织自己的 npm 代理地址:
registry=https://registry.example.com/repository/npm-group/
ignore-scripts=true
save-exact=true
package-lock=true
在 CI 中使用不可变安装,避免流水线临时解析出与审查时不同的版本:
set -euo pipefail
npm ci --ignore-scripts
npm audit --audit-level=high
使用 pnpm 的团队还可以在支持该能力的版本中设置最小发布时间,并仅允许经过确认的软件包执行构建脚本。具体字段支持情况应以当前 pnpm 版本为准:
# pnpm-workspace.yaml
minimumReleaseAge: 1440
onlyBuiltDependencies:
- esbuild
1440 分钟意味着新版本至少等待 24 小时。冷却期不能证明软件包安全,但能为社区发现账号接管、恶意发布和异常安装脚本争取时间。旧版包管理器可能忽略相关配置,落地策略时必须把客户端最低版本检查加入 CI。
Python 项目同样不应允许构建机绕过组织策略直接访问公共 PyPI。可以这样配置 pip.conf:
[global]
index-url = https://pypi.example.com/simple
trusted-host = pypi.example.com
require-virtualenv = true
disable-pip-version-check = true
随后使用哈希锁定依赖。下面假设 requirements.txt 已由组织批准的锁定工具生成,并包含每个制品的 --hash:
python -m venv .venv
. .venv/bin/activate
python -m pip install --require-hashes -r requirements.txt
这里的关键不是某个配置项,而是建立唯一入口:开发机、CI 和生产镜像都只能从经过审查的内部仓库获取依赖。否则攻击者只需找到一条仍能直连公网的安装路径,就可以绕过冷却和隔离策略。
流水线要按不可信代码执行环境设计
依赖包和外部 Pull Request 都属于不可信输入。Runner 不应拥有长期存在的发布令牌,也不应与生产网络处在同一信任区域。
尤其需要审计 GitHub Actions 的 pull_request_target。该触发器在基础仓库上下文中运行,可能接触仓库密钥和写权限。如果工作流随后检出并执行外部贡献者的代码,攻击者就可能直接读取凭据。
处理普通外部贡献时,可以这样实践:使用权限较低的 pull_request,明确收紧默认权限,并禁止安装脚本:
name: pull-request-check
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts
- run: npm test
如果业务确实需要 pull_request_target,不要在同一个高权限作业中检出或执行 PR 分支代码。把标签、评论等元数据操作与代码测试拆成独立工作流,并通过人工审批或经过验证的制品传递结果。
流水线身份也应从长期 PAT 转向 GitHub App 或 OIDC 短期凭据。云端角色只允许特定仓库、分支和工作流声明进行交换,令牌在几分钟后失效。配合一次性 Runner、最小网络出口和只读文件系统,即使恶意依赖成功执行,其可窃取的内容和可到达的系统也会明显减少。
用 SBOM、ABOM 和 OSV 扩大可见性
SBOM 回答“产品里有什么软件”,但供应链排查还需要回答“谁构建了它”。因此还应维护 ABOM,记录 GitHub Actions、第三方构建插件、CI 服务和开发工具,并把这些动作关联到最终容器镜像或发布制品。
可以在 CI 中加入 OSV-Scanner,对锁文件或源码目录中的依赖进行已知漏洞检查。安装方式和参数可能随版本演进,接入前应核对当前发布说明;一个基础流水线可以这样实践:
set -euo pipefail
osv-scanner scan source -r .
osv-scanner scan image "registry.example.com/team/app:${GIT_COMMIT}"
扫描结果不应被当作供应链安全的全部。OSV 擅长把已知漏洞映射到明确版本范围和提交,但它无法保证一个刚发布、尚未被报告的包没有恶意行为。更完整的准入流程还应包括:
- 生成并集中保存 SBOM,持续与新披露漏洞交叉比对。
- 校验制品哈希、数字签名和来源证明,构建过程逐步对齐 SLSA。
- 在隔离沙箱中观察安装脚本、网络连接和文件写入行为。
- 检查依赖维护者的恢复邮箱域名,识别已过期或可重新注册的域名。
- 监控工作流修改、异常触发器、未知出口地址和长期密钥的异地使用。
落地顺序:先切断可串联的攻击链
供应链风险不应被拆成互不相关的告警。一次严重入侵往往由多个普通问题串联而成:锁文件可被绕过,Runner 拥有写权限,OIDC 信任条件过宽,同时生产网络允许任意出站连接。治理时应按攻击链排序,而不是只按单个漏洞的 CVSS 分数排序。
建议按以下顺序推进:
- 盘点应用、直接依赖、传递依赖、CI 动作、制品仓库和发布身份,建立 SBOM 与 ABOM。
- 强制所有包管理器使用内部仓库,启用新版本冷却、锁文件和哈希验证。
- 默认禁用 npm 生命周期脚本,只为必要且经过审查的包建立允许列表。
- 清理长期 PAT 和发布密钥,改用短期 OIDC 身份、GitHub App 与硬件保护的签名密钥。
- 审计
pull_request_target、repository_dispatch等高风险触发器,并将外部代码与高权限作业隔离。 - 使用一次性 Runner、严格出口白名单和生产网络分段限制恶意代码的影响范围。
- 为维护者账号接管、依赖混淆、流水线凭据收割和开发者工作站失陷分别演练响应流程。
平台原生安全默认值会持续变化,来源材料也提到了包管理器、Dependabot 与 PyPI 侧的进一步保护计划。团队不能预设这些能力已经覆盖所有环境;上线前应核对实际版本、托管平台文档和组织策略的生效结果。真正稳健的供应链防线依靠多层控制:延迟未经验证的变更、隔离不可信执行、缩短凭据寿命,并保留足够完整的证据来快速定位和撤回受污染制品。