软件供应链安全:你的依赖里藏着多少把刀

2026-06-04 26 预计阅读时间:1 分钟
来源:docker.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

2025 年,开源仓库里冒出了超过 454,000 个恶意包——这不是漏洞数量,是有人故意投毒的包数量。Sonatype 的 2026 年《软件供应链状态报告》把累计数字摆到了桌面上:自 2019 年以来,恶意包总量突破 120 万。绝大多数团队根本没意识到自己装了多少依赖,更不知道这些依赖的上游是否还安全。供应链攻击的爆炸半径正在随依赖深度指数级膨胀,而你每天 pip installnpm install 的那一刻,就是攻击者进入你系统的入口。

攻击者怎么往你的依赖里掺毒

供应链攻击不是新概念,但手法在过去两年里快速进化。三种最常见的模式:

同名仿冒(Typosquatting)——攻击者注册一个和流行包名字极其相似的包,等你手滑敲错一个字母就装上了毒。比如 reqeusts 伪装 requestsloddash 伪装 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.txtpackage.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

采纳路径与取舍

供应链安全没有银弹,但有一条清晰的优先级路径:

  1. 先锁版本、先审计——这是成本最低、收益最高的第一步。不锁版本,后面所有防线都有被绕过的风险。
  2. 切私有镜像——如果你有内部包,这一步必须做。依赖混淆攻击不需要任何技术突破,只需要版本号更高。
  3. CI 门禁自动化——人工审计不可能跟上 120 万恶意包的规模,必须让 CI 替你盯着。
  4. SBOM + 签名——这是长期基建,适合已经成熟的项目和组织。前期投入大,但在合规和溯源场景价值极高。

取舍提醒:

  • 完全切断公网源意味着你需要自己维护镜像的同步和更新,私有镜像落后于上游会拖慢开发。
  • 锁版本会阻止自动获取安全补丁版本,必须建立定期升级流程(建议每月一次集中升级 + 审计)。
  • SBOM 生成会增加构建时间约 10–30 秒,CI 资源消耗也会上升。
  • typosquat 检测的阈值设太低会误报,设太高会漏报,需要根据团队实际依赖列表调校。

供应链安全的核心认知转变是:你不仅要信任自己的代码,还要持续验证你借来的每一行代码的来源。 120 万恶意包的数字不会停止增长,但你的防线可以从今天开始。


相关推荐