纵深防御:Python供应链安全实用指南
摘要
一份关于通过分层防御保护Python供应链的实用指南,包括使用Ruff进行代码检查、使用哈希锁定依赖项、使用pip-audit进行漏洞扫描、生成SBOM以及使用OIDC证明的可信发布。
<p>层层设防,不要信任任何单一控制措施。使用带安全规则的Ruff在代码发布前捕获bug。使用uv lock或uv pip compile --generate-hashes将所有依赖项用加密哈希锁定,这样没人能替换你的包。在CI中运行pip-audit,在已知CVE进入生产环境前捕获它们。使用CycloneDX生成SBOM,这样当下一次出现Ultralytics式的入侵时,你可以在几分钟而非几天内回答“我们是否受影响?”</p>
<p>如果你在发布包,放弃长期有效的API令牌,改用带有OIDC的可信发布。这会通过Sigstore自动生成证明,将你的包链接回源代码仓库。运行内部镜像的组织可以添加7天延迟,让社区充当你的金丝雀——但前提是你有维护它的基础设施。</p>
<p>这里没有什么是完美的。哈希锁定可以防止篡改,但无法拯救你最初安装的恶意包。扫描能发现已知CVE,但会遗漏零日漏洞。证明能证实代码的来源,但不能证明其安全性。这就是为什么你要分层设防——当一个控制失效时,其他控制会补上。从代码检查和锁定开始快速见效,接着添加扫描和SBOM,然后随着你成熟逐步升级到高级内容。</p>
<p><a href="https://lobste.rs/s/ghsneu/defense_depth_practical_guide_python">评论</a></p>
查看缓存全文
缓存时间: 2026/04/20 14:44
# 深度防御:Python供应链安全实用指南
来源:https://bernat.tech/posts/securing-python-supply-chain/
TLDR:分层防御,不要信任任何单一控制措施。使用 Ruff 的安全规则在代码发布前捕获错误。使用 `uv lock` 或 `uv pip compile --generate-hashes` 锁定所有依赖项的加密哈希,防止他人替换包。在 CI 中运行 `pip-audit` (https://github.com/pypa/pip-audit) 在生产环境前捕获已知 CVE。生成 CycloneDX 格式的 SBOM,以便下次出现类似 Ultralytics 的入侵事件时,你可以在几分钟(而不是几天)内回答“我们受影响了吗?”如果你在发布包,请放弃长期有效的 API 令牌,改用基于 OIDC 的受信任发布。这会通过 Sigstore 自动生成凭证,将你的包与源代码仓库关联起来。运行内部镜像的组织可以添加 7 天的延迟,让社区充当你的“金丝雀”——但前提是你有足够的基础设施来维护它。这里没有完美的方案。哈希锁定可以防止篡改,但无法挽救你第一天就安装的恶意包。扫描可以发现已知 CVE,但会遗漏零日漏洞。凭证可以证明代码来自哪里,但不能证明其安全性。这就是为什么你要分层部署——当一层控制失效时,另一层会接住它。从 linting 和锁定开始获得快速收益,接着添加扫描和 SBOM,然后随着成熟度提升再升级到高级措施。
我维护着多个 PyPA 项目(virtualenv、tox、pipx、platformdirs、filelock),并从事企业级包托管基础设施工作。多年来,我亲眼目睹了针对 Python 包的供应链攻击变得越来越恶劣——无论是作为开源维护者向 PyPI 发布包,还是作为企业消费者管理数千个依赖项。本文涵盖了保护 Python 供应链的实用方法。如需了解涵盖所有生态系统的更广泛威胁模型,CNCF 软件供应链安全白皮书 (https://tag-security.cncf.io/community/working-groups/supply-chain-security/supply-chain-security-paper-v2/Software_Supply_Chain_Practices_whitepaper_v2.pdf) 是一个极好的入门读物。这里我们将专注于 Python 特定的防御措施——编写安全代码、管理依赖项、扫描漏洞以及验证包的 authenticity。
## 为什么这很重要
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#why-this-matters)
我们面对的数据规模:截至 2026 年 3 月,PyPI 托管着超过 743,000 个包,而且这个数字每天都在增长。你的普通 Python 项目通常会引入数十个传递依赖——那些你从未明确选择但你的依赖项需要它们,所以你不得不依赖的包。关键在于:安全补丁总是滞后于漏洞发现,有时滞后数周甚至数月。
从开发者到应用程序的流程:
```mermaid
flowchart TB
Dev[开发者编写代码] --> Build[构建包]
Build --> Upload[上传到 PyPI]
Upload --> PyPI[PyPI]
PyPI --> Install[你的 pip install]
Install --> App[你的应用]
Attacker[攻击者] -.->|入侵| Dev
Attacker -.->|恶意包| PyPI
Attacker -.->|域名抢注| PyPI
style Attacker fill:#dc2626,stroke:#b91c1c,color:#fff
style App fill:#50b432,stroke:#3d8a26,color:#fff
linkStyle 5 stroke:#dc2626
linkStyle 6 stroke:#dc2626
linkStyle 7 stroke:#dc2626
```
注意到所有这些红色箭头了吗?每个都代表一个潜在的攻击向量。真实事件证明了为什么这很重要。
### 真实攻击,真实影响
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#real-attacks-real-impact)
**ctx 和 PHPass 账号劫持 (https://www.crowdstrike.com/en-us/blog/how-crowdstrike-detects-poisoned-python-packages-ctx-phpass/)(2022 年 5 月)**:攻击者通过重新注册 `ctx` 包维护者的过期邮箱域名,入侵了该包(自 2014 年以来未更新)。他们推送了一个恶意更新,将 AWS 凭证和其他敏感环境变量泄露到攻击者控制的服务器。报告指出,在检测到之前大约 10 天内,每天约有 2,000 次下载,可能暴露了许多 AWS 账户。此后,PyPI 实施了域名复活预防措施 (https://blog.pypi.org/posts/2025-08-18-preventing-domain-resurrections/)——检测过期域名并取消验证相关邮箱地址,以缓解此类攻击。
**Ultralytics 入侵事件 (https://blog.pypi.org/posts/2024-12-11-ultralytics-attack-analysis/)(2024 年 12 月)**:广泛使用的 YOLO 计算机视觉包(截至 2024 年 12 月报告每月约 8000 万次下载)因 GitHub Actions 脚本注入攻击被入侵。攻击者窃取了 PyPI 上传令牌,并向四个版本注入了加密货币挖矿程序。成千上万的开发者在运行 `pip install ultralytics` 时不知情地安装了恶意软件。
**PyPI 钓鱼活动 (https://blog.pypi.org/posts/2025-07-28-pypi-phishing-attack/)(2025 年 7 月)**:发布包且元数据中包含邮箱的维护者收到了来自 `[email protected]`(注意小写的 `j`)的钓鱼邮件。攻击使用了一个代理凭证窃取器,将窃取的凭证传递给真实的 PyPI,使受害者认为他们正常登录了。PyPI 随后对基于 TOTP 的登录实施了登录验证 (https://blog.pypi.org/posts/2025-11-14-login-verification/),要求从未识别设备登录时进行验证。
**GhostAction 攻击 (https://blog.pypi.org/posts/2025-09-16-github-actions-token-exfiltration/)(2025 年 9 月)**:威胁行为者向超过 570 个仓库的 GitHub Actions 工作流注入了代码,窃取了超过 3,300 个机密,包括 PyPI 令牌、npm 令牌和 AWS 访问密钥。PyPI 作废了所有被窃的令牌 (https://www.bleepingcomputer.com/news/security/pypi-invalidates-tokens-stolen-in-ghostaction-supply-chain-attack/),并推动所有人迁移到受信任发布者 (https://bernat.tech/posts/securing-python-supply-chain/#the-new-way-trusted-publishing)。
**Shai-Hulud 蠕虫活动 (https://blog.pypi.org/posts/2025-11-26-pypi-and-shai-hulud/)(2025 年 11 月)**:一种跨生态系统的蠕虫,主要针对 npm,但也影响到了 PyPI,因为单体仓库设置会同时存储两个注册表的凭证。攻击者入侵了 npm 账户,并从 GitHub 仓库机密中泄露了长期有效的 PyPI 令牌。PyPI 主动撤销了暴露的令牌,并建议使用 zizmor (https://docs.zizmor.sh/) 审计 GitHub Actions 工作流。
这些不是理论上的攻击。它们发生在拥有数百万用户的真实项目上。如果你在 PyPI 上发现恶意包,可以通过 PyPI 的安全报告系统 (https://pypi.org/security/) 报告。
### 隐藏的依赖问题
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#the-hidden-dependency-problem)
当你安装 Flask 时,你得到的不仅仅是 Flask。以下是完整的依赖树:
```bash
# 安装 Flask
uv pip install flask
# 显示完整的依赖树
uv pip tree
# 输出:
# flask v3.1.0
# ├── blinker v1.9.0
# ├── click v8.1.8
# ├── itsdangerous v2.2.0
# ├── jinja2 v3.1.5
# │ └── markupsafe v3.0.2
# └── werkzeug v3.1.3
# └── markupsafe v3.0.2
```
看到了吗?你请求了一个包(Flask),却得到了七个。看看最底部的 MarkupSafe——它是由 Jinja2 和 Werkzeug 共同引入的传递依赖。你从未显式安装过它。你可能甚至不知道它做什么。但如果它有漏洞,你的应用程序就会变得脆弱。每个项目平均有 50 多个传递依赖,你的攻击面比你 requirements 文件中看到的大得多。
现在让我们构建你的防御策略,从你自己的代码开始。
## 首先保护你自己的代码
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#secure-your-own-code-first)
供应链攻击不仅来自外部依赖——你自己的代码也可能创建入口点。一个硬编码在源代码中的 PyPI 令牌,一旦推送到仓库,就会给攻击者一切所需,入侵你的账户并在你的名下发布恶意包。
除了机密之外,常见的安全漏洞隐藏在日常看起来完全正常的代码模式中——而人在时间压力下会忽略这些。用 linter 自动捕获它们是第一层防御。
### 永久的秘密
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#the-forever-secret)
泄露的凭证是许多供应链入侵的起点。暴露的 PyPI 令牌让攻击者可以发布你包的带后门版本。暴露的数据库 URL 让他们可以泄露数据。然而,这种模式令人沮丧地常见:
```python
# 不好的做法:代码中的秘密在 git 历史中永存
SECRET_KEY = "hunter2"
DATABASE_URL = "postgres://admin:password123@prod-db:5432/app"
# 好的做法:使用环境变量
import os
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
```
Git 永远不会忘记。当你提交一个秘密时,它会永远存在于仓库的历史中。在后续提交中删除它也无济于事——任何有仓库访问权限(或有旧克隆)的人都可以提取那些凭证。攻击者经常在 git 历史中搜索秘密,泄露的 PyPI 令牌或云凭证往往是供应链入侵的第一步。
### 损坏的加密
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#broken-cryptography)
另一个常见漏洞:
```python
# 不好的做法:MD5 和 SHA1 已损坏
import hashlib
digest = hashlib.md5(payload).hexdigest()
# 好的做法:使用 SHA256 或更好的算法
digest = hashlib.sha256(payload).hexdigest()
```
MD5 碰撞首次演示于 2004 年,尽管其弱点更早便已知晓。SHA1 实际碰撞于 2017 年演示。NIST 早在 2011 年就弃用了两者用于数字签名。“已损坏”意味着攻击者可以生成碰撞——不同的输入产生相同的哈希值。这会导致证书伪造、下载篡改或完整性检查绕过。不要为了安全目的使用它们。
### 挂起的连接
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#the-hanging-connection)
这个问题很微妙但危险:
```python
# 不好的做法:在缓慢/恶意服务器上无限挂起
import requests
response = requests.get("https://api.example.com/data")
# 好的做法:始终设置超时
response = requests.get("https://api.example.com/data", timeout=30)
```
没有超时,一个缓慢或恶意的服务器可以让你的进程无限挂起。攻击者控制了你应用程序通信的服务器,可以让每个请求挂起,耗尽你的线程池,造成拒绝服务。因为你忘记了一个参数,整个应用程序就会瘫痪。
### 用 Ruff 捕获这些问题
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#catch-these-with-ruff)
Ruff (https://docs.astral.sh/ruff/) 是一个速度极快的 Python linter,包含来自 Bandit (https://bandit.readthedocs.io/en/latest/) 的全面安全规则。你可以在 Ruff 安全规则文档 (https://docs.astral.sh/ruff/rules/#flake8-bandit-s) 中了解更多。在 `pyproject.toml` 中配置它:
```toml
# 从错误、pyflakes 和安全规则开始
[tool.ruff]
line-length = 120
lint.select = ["E", "F", "S"]
```
仅安全规则(`"S"`)就提供了显著价值——它们是 Bandit 检查,用于捕获硬编码的机密、弱加密和不安全的反序列化。当你的代码库干净后,扩展到所有规则:
```toml
# 进取目标:启用所有规则并有选择地忽略
[tool.ruff]
line-length = 120
lint.select = ["ALL"]
lint.ignore = [
"COM812", # 与格式化器冲突
"CPY", # 无版权
"D", # pydocstyle:后续为公共 API 启用(如果发布库)
"ISC001", # 与格式化器冲突
]
```
Ruff 运行时间不到一秒,因此你可以在 IDE 中边输入边运行,并在每次提交前运行。上述三个漏洞都会被自动捕获:
- **S105** (https://docs.astral.sh/ruff/rules/hardcoded-password-string/) - 硬编码的机密
- **S324** (https://docs.astral.sh/ruff/rules/hashlib-insecure-hash-function/) - 弱加密
- **S113** (https://docs.astral.sh/ruff/rules/request-without-timeout/) - 缺少超时
- **S301** (https://docs.astral.sh/ruff/rules/suspicious-pickle-usage/) - pickle 和其他不安全的反序列化
- **S608** (https://docs.astral.sh/ruff/rules/hardcoded-sql-expression/) - 通过字符串格式化的 SQL 注入
- **S307** (https://docs.astral.sh/ruff/rules/suspicious-eval-usage/) - 使用 eval() 处理不受信任的输入
每个链接的规则页面都包含对危险模式原因的详细解释、易受攻击代码的示例以及如何修复——如果你想知道风险而不仅仅是忽略警告,值得一读。
例如,这种危险模式会立即被标记:
```python
# 被标记:S301 - pickle.loads() 可以执行任意代码
import pickle
data = pickle.loads(untrusted_input)
# 改为使用 json.loads()
# 被标记:S608 - SQL 注入漏洞
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
```
将 Ruff 添加到你的编辑器和 CI 流水线——它会拯救你健忘的自己。
## 管理你的依赖项
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#manage-your-dependencies)
现在让我们谈谈管理你未编写的代码——你的依赖项。这才是供应链攻击实际发生的地方。OpenSSF 安全供应链消费框架 (S2C2F) (https://github.com/ossf/s2c2f) 提供了一个结构化的成熟度模型,指导组织应如何消费开源软件。
### 谨慎选择依赖项
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#choose-dependencies-carefully)
在添加依赖项之前,考虑你是否真的需要它。每个依赖项都会扩大你的攻击面——更少的依赖项意味着更少的被入侵机会。当你确实添加一个时,使用 OpenSSF Scorecard (https://securityscorecards.dev/) 评估发布者的安全态势,该工具根据分支保护、签名发布、依赖项更新工具和漏洞披露等实践对项目进行评分。低分并不意味着“不要使用它”,但它告诉你你对一个安全卫生有限的项目投入了多少信任。
### 未固定的依赖问题
链接到标题 (https://bernat.tech/posts/securing-python-supply-chain/#the-unpinned-dependency-problem)
一个比你想象中更常发生的场景:你在 requirements 文件中写了 `flask>=2.0`。今天,当你运行 `pip install` 时,你会得到 Flask 3.1.0,一切正常。明天,攻击者发布了一个被入侵的 Flask 3.1.1。你下一次 `pip install` 会悄悄下载恶意版本,因为它满足你的 `>=2.0` 约束。你刚在没更改一行代码的情况下安装了恶意软件。
从不安全到安全的演进:
```mermaid
graph LR
A[未固定: flask>=2.0] -->|获得任意版本| B[危险]
C[版本固定: flask==3.1.1] -->|获得确切版本| D[更好]
E[哈希固定: flask==3.1.1--hash=sha256:...] -->|验证内容| F[安全]
style B fill:#dc2626,stroke:#b91c1c,color:#fff
style D fill:#f59e0b,stroke:#d97706,color:#fff
style F fill:#50b432,stroke:#3d8a26,color:#fff
```
**未固定**(`flask>=2.0`)是最危险的——你会得到任何最新版本,可能已被入侵。你的构建不可重现,也无法检测篡改。
**版本固定**(`flask==3.1.1`)更好——你得到你测试过的确切版本。但没有完整性检查。如果攻击者入侵了维护者的账户,并为相同版本发布了新的带后门构件(例如,之前未上传过的针对某个平台的 wheel),你会安装它。
相似文章
npm/Docker/PyPI的供应链安全模式正在MCP上重演,我们正处于2015年的时刻
文章警告称,MCP生态正在重演npm、Docker和PyPI中出现的供应链安全模式——审核极少,风险日益增长。文章指出,对500个Smithery服务器的扫描发现18.8%存在安全问题,现有安全工具无法处理恶意智能体指令,并介绍了一个名为bawbel的新型静态扫描器。
@altryne: 公开提醒:如果你尚未了解最新的供应链攻击,或者知道但无动于衷且未采取任何措施,尤其是……
一则关于通过 npm 和 PyPI 针对 AI 开发者工具(Hermes、OpenClaw)的供应链攻击的公开提醒,特别是名为“Mini-Shai Hulud”的蠕虫病毒,它能自我复制并窃取凭据、API 密钥和浏览器会话。文章建议使用沙盒执行并限制软件包年龄以降低风险。
AI代理是否正在创造一个新的运行时供应链攻击面?
讨论AI代理安全作为一个超越提示注入的运行时供应链问题,强调来自不可信数据、工具和反馈循环的风险,并质疑开发者如何执行边界。
构建我梦寐以求的部署工具
作者详细介绍了一款名为'Deptool'的自定义Python部署与配置管理工具的开发过程。该工具旨在比Ansible等现有方案更快、更可预测,源于对数字主权和更优工具的追求。
@DeRonin_: 使用本指南保护你的计算机免受 NPM 攻击,这些攻击会在一次安装中窃取一切。TanStack,一个代码库使用……
本文详细介绍了针对 NPM 上 TanStack 库的供应链攻击,并提供了一份全面的指南,通过锁定依赖项发布年龄、固定版本以及对 CI/CD 流水线和 IDE 扩展进行审计,来保护开发环境的安全。