AI 模型是一个神经网络:一套规定如何计算的架构,加上数百万到数十亿个学习得到的数值,也就是权重。但装着它的文件可以包含更多东西:在加载时被执行的指令。不加检查就加载别人给的 .pt 文件,最坏的情况下等于启动了一段别人写的程序。
Hugging Face 上已有超过两百万个公开模型,而大多数团队下载它们时,就像安装一个 npm 包一样不设防。这篇文章是关于模型文件的深入篇:哪些格式能执行什么,扫描器会漏掉什么,以及如何在第三方模型进入生产之前检查它。从数据到智能体工具的整条供应链概览,见《AI 供应链的新风险》。
模型文件何时会变成程序
许多模型格式建立在 Python 的 pickle 之上。pickle 保存的不只是数值,还有一串“如何重建对象”的指令。通过 __reduce__ 方法,对象可以指定任意函数和任意参数,加载器会照单执行。Python 官方文档说得很直白:这个模块并不安全,只应反序列化你信任的数据。
import os, pickle
class Model:
def __reduce__(self):
# 任意命令
return (os.system, ("id",))
# 看起来像个模型
pickle.dump(Model(), open("model.pkl", "wb"))
# 实际执行了命令
pickle.load(open("model.pkl", "rb"))
所以,加载时会发生什么,取决于格式:
| 格式 | 加载时会发生什么 | 剩余风险 |
|---|---|---|
| .pkl、.pt、.bin Pickle |
文件一被加载,就可能执行任意代码。 | 高 只加载你愿意当作自己代码对待的文件。 |
| .safetensors | JSON 头部加原始张量数据,没有可执行内容。Trail of Bits 在 2023 年的审计没有发现会导致代码执行的严重漏洞。 | 低 权重本身仍可能带有后门。 |
| .h5、.keras Keras |
Lambda 层// Lambda 层Keras 模型中的一种构件,可以把任意 Python 代码作为计算步骤嵌入模型。它以字节码形式保存,在加载模型时就可能被执行。例子一个名字看似无害的层 scale_inputs,暗中向外建立连接。把 Python 代码带进了模型架构(CVE-2024-3660)。 |
中 检查各层,保持 Keras 为最新版本。 |
| .gguf// GGUF用于在本地运行语言模型的文件格式,例如配合 llama.cpp 使用。 | 不使用 pickle,但文件里带有聊天模板:在 llama-cpp-python 中,这曾导致代码执行(CVE-2024-34359);2025 年 Pillar Security 还展示了在推理时注入指令的模板。 | 中 阅读模板,给运行时打补丁。 |
有两点很容易被忽略。第一:weights_only=True 从 PyTorch 2.6 起成为默认值,它会限制反序列化器的能力。但这不是保证:在 2.5.1 及更早版本中,CVE-2025-32434 即使设置了该参数仍可执行代码。第二:Safetensors 防的是加载时的代码,防不了权重里“学到”的东西。
架构中的代码:Keras Lambda 层
pickle 并不是把代码带进模型的唯一途径。Keras 提供 Lambda 层:它可以把任意 Python 表达式作为一个计算步骤挂进模型。按照 Keras 文档 的说法,这类层以 Python 字节码的形式保存,文档自己就称之为“potentially unsafe”。
CERT/CC 对后果的描述很直接:加载第三方的 Keras 模型,可能就是在运行第三方的代码。因此 Keras 3 默认拒绝加载函数以字节码形式保存的 Lambda 层(safe_mode)。但还有两个缺口。在 Keras 2 以及 3.12 之前的 Keras 3 中,这项检查对经典的 .h5 文件并不生效,CERT/CC 的说明也是这样写的。直到 2025 年 10 月发布的 3.12,Keras 才把 safe_mode 传递给 H5 加载器。而这层保护本身也在 2025 年被绕过:CVE-2025-1550 在开启 safe_mode 的情况下仍可执行代码。
换格式在这里帮不上忙:Safetensors 只保存权重,架构仍然留在 Keras 文件或代码里。有用的做法是在加载模型之前先看一眼层列表。ModelScan 会在 .h5 文件中专门查找 Lambda 层(安装时带上附加组件 modelscan[tensorflow,h5py])。只要模型不是你自己构建的,其中出现的任何 Lambda 层或自定义层都值得追问。
模型文件已经出过哪些事
2024 年 2 月,JFrog 在 Hugging Face 上发现了大约 100 个模型会在加载时执行代码,其中一个会建立反向 shell// Reverse Shell被入侵的机器主动连接攻击者,并把命令行交给对方。。JFrog 自己也指出,这些载荷// Payload(载荷)攻击中真正造成破坏的那部分。这里指被植入的代码。中大多数可能是研究人员或漏洞赏金猎人的测试。但原理同样成立。
2025 年以来的三个变化让情况更加棘手。
1 // 扫描器可以被骗过
Hugging Face 会扫描上传的文件,但它自己也说这并非百分之百可靠。2025 年 2 月,ReversingLabs 披露了两个模型(nullifAI):它们用 7z 而不是 ZIP 压缩,pickle 流在载荷之后立即中断。它们没有被标记为不安全,载荷却照样执行。同一年,Sonatype 报告了四种、JFrog 又报告了三种绕过 picklescan 扫描器的方法。平台上的绿色对勾只是一个提示,不是结论。
2 // 名字不能证明来源
PoisonGPT 在 2023 年演示了这个原理:一个被刻意篡改的模型,上传在 EleuterAI 名下,而不是 EleutherAI。2025 年,Unit 42 描述了模型命名空间重用(Model Namespace Reuse):作者账号被删除或转移后,别人可以重新注册原来的名字,所有只按名字加载模型的流水线都会拉到陌生人的模型。据 Unit 42 称,Google 的 Vertex AI 和微软的 Azure AI Foundry 也受到影响。2026 年 5 月,一个仿冒 OpenAI 发布内容的仓库在被封禁之前登上了 Hugging Face 趋势榜第一名。据 HiddenLayer 判断,它的下载量几乎可以肯定是人为刷出来的。
3 // 适配器与转换器
模型很少单独出现。LoRA 适配器// LoRA 适配器一种小型附加文件,无需重新训练就能让基础模型适配某项任务。例子把通用语言模型调整为客服回复风格的适配器。和模型一样被四处分享,而研究人员证明,后门只需在一个适配器里训练一次,之后无需再训练就能与其他适配器合并。因此 OWASP LLM 应用十大风险把存在漏洞的 LoRA 适配器单独列为一项供应链风险。2024 年,HiddenLayer 展示了 Hugging Face 的 Safetensors 转换服务可以被劫持,从而以官方机器人的名义提交 Pull Request。凡是经过别人流水线的东西,都应视为一个新的制品。
风险、措施、工具
用一张表概括。这里提到的工具全部开源。
| 风险 | 措施 | 工具 |
|---|---|---|
| 模型文件中的代码 | 只允许 Safetensors 一种格式。绝不未经检查就加载 pickle。使用 PyTorch 2.6 及以上版本。 | safetensors、Fickling、ModelAudit、ModelScan、picklescan |
| 架构中的逻辑 | 加载前先列出所有层。保持 Keras 的 safe_mode 开启,并及时更新 Keras。 |
ModelScan、h5py |
| 文件被替换 | 固定哈希和修订版本,校验签名。 | sha256sum、带完整提交哈希的 revision=、OpenSSF Model Signing |
| 伪造或被接管的仓库 | 为组织建立白名单。生产环境只从自己的注册表加载,绝不按名字直接从 Hub 拉取。 | 内部模型注册表、提交哈希固定 |
| 加载时触发载荷 | 首次加载只在没有网络的沙箱中进行,并采集遥测。 | 禁止出站流量的容器、sys.addaudithook |
| 权重中的后门、被投毒的数据 | 用你知道正确答案的用例做自己的评测。上线前做红队测试。 | garak、promptfoo |
| 适配器与转换过的模型 | 把它们当作新模型对待:同样的关卡,单独扫描,单独批准。 | 与上面相同的扫描器 |
软件包、供应商和智能体工具各有各的风险,这些内容在概览篇里:《AI 供应链的新风险》。
工具清单里不会写的三个局限
没有哪个扫描器是完备的。Fickling 自己就提醒,文件即使通过检查也可能不安全;而 ModelScan 最近一次发布是在 2026 年 2 月。请准备两个扫描器,而不是一个。按照 Python 文档,审计钩子不是沙箱,恶意代码甚至可以把它关掉:它提供遥测,隔离要靠容器。另外,pip-audit 找的是已知漏洞,不是恶意软件包,它的文档明确说明了这一点。
五道关卡的准入流水线
如果模型能绕过工具直接进入生产环境,单个工具的作用就很有限。对策是一套固定的准入流程:五道关卡,无论来源声誉多好,每个制品都必须逐一通过。
从下载到批准的五道关卡。生产环境只加载通过第 5 道关卡的制品。
- 1. 隔离。下载到隔离的暂存环境,绝不直接进入生产。首次加载在没有网络的条件下进行。
- 2. 来源。组织、账号年龄、历史记录、模型卡。按提交哈希加载,而不是按名字。
- 3. 完整性。用 SHA-256 与公布的值比对,有签名就校验签名。OpenSSF Model Signing 会对一个模型所有文件的清单进行签名。NVIDIA 表示,自 2025 年 3 月起,它在 NGC 目录中发布的所有自有模型都以这种方式签名。
- 4. 扫描。对文件做静态扫描,Keras 看层列表,GGUF 看聊天模板,并审计依赖。
- 5. 批准。到这一步,模型才进入内部注册表。
自己动手:三分钟完成一次扫描
你需要一个终端(macOS 或 Linux,Windows 下用 WSL)和 Python 3.10 到 3.12。三个扫描器都通过 pip 安装,最好装进一个全新的虚拟环境:
python3 -m venv .venv
source .venv/bin/activate
pip install fickling modelscan modelaudit
然后生成一个无害的测试文件。它用的是和上文相同的手法,只不过命令仅仅输出一个单词:
python3 - <<'EOF'
import os, pickle
class Model:
def __reduce__(self):
return (os.system, ("echo pwned",))
pickle.dump(Model(), open("test.pkl", "wb"))
EOF
现在在不加载文件的前提下检查它:
# 标准库自带:显示 pickle 中的指令
python3 -m pickletools test.pkl
# 把 pickle 还原成 Python 代码并给出评估
fickling --check-safety -p test.pkl
# 扫描并给出严重级别
modelscan -p test.pkl
modelaudit test.pkl
三个扫描器都会报警。Fickling 把这个文件称为“overtly malicious”,ModelScan 的输出是:
Unsafe operator found:
- Severity: CRITICAL
- Description: Use of unsafe operator 'system' from module 'posix'
posix 是 macOS 和 Linux 上 os.system 背后的模块。关键在于你没有做的那件事:你从头到尾都没有用 pickle.load() 打开这个文件。
用在真实项目上
用的还是同一套工具。加载时固定版本,这里用的是 transformers 库。org/model 代表 Hugging Face 上的仓库名,提交哈希可以在该仓库的历史记录里找到:
from transformers import AutoModel
model = AutoModel.from_pretrained(
"org/model",
# 完整提交哈希,而不是 "main"
revision="877b84a8f93f2d619faa2a6e514a32beef88ab0a",
use_safetensors=True, # 不用 pickle
trust_remote_code=False, # 默认值,保持不变
)
这里的 commit hash 不只是装饰。如果仓库里没有 safetensors 文件,而 revision 又没有固定,transformers 会尝试通过 Hugging Face 转换机器人的 Pull Request 分支做自动转换,也就是“Silent Sabotage”中的那个服务。固定了 commit hash 就不会发生这种情况。而不设置 use_safetensors=True 时,库会悄悄退回到 pickle 文件。
第 3、4 道关卡的检查在存放 requirements.txt 的项目目录下运行:
# 第 3 道关卡:把哈希与公布的值比对
sha256sum model.safetensors
# 第 4 道关卡:从 Hub 获取仓库并做静态扫描
modelaudit hf://org/model
# 第 4 道关卡:检查依赖中的已知漏洞
pip install pip-audit
pip-audit -r requirements.txt
# 第 4 道关卡:生成物料清单并检查
syft . -o cyclonedx-json=sbom.json
grype sbom:./sbom.json
三点说明。Syft 和 Grype 是独立的程序,不是 Python 包,在 Mac 上用 brew install syft grype 安装。在较旧的 macOS 版本上,sha256sum 要换成 shasum -a 256。另外,只有当 requirements.txt 的每一行都带有哈希时,pip-audit --require-hashes -r requirements.txt 才能以严格模式运行;没有哈希,这条命令会直接中止。
只有发布者提供了签名,才谈得上校验签名。这时你需要签名文件、发布者的预期身份,以及其登录服务提供方:
pip install model-signing
model_signing verify ./model --signature model.sig \
--identity "releases@example.org" \
--identity_provider "https://accounts.example.org"
适配器和转换过的模型走同一条路。API 模型、MCP 服务器和提示词模板有哪些不同,见概览篇:《AI 供应链的新风险》。
工具解决不了的事
无论流水线做得多好,有三件事始终存在。
- 权重无法阅读。训练进去的后门,静态扫描器找不到。能起作用的只有自己的测试,以及对运行时行为的观察。
- 答对了也说明不了什么。载荷在加载时执行。此后模型的回答可以完全正常。
- 信任不是控制手段。最棘手的案例都来自合法渠道:一个登上趋势榜第一的仓库、一个顶着已注销作者名字的模型、平台自己的转换服务。
如果现在什么都没有,我会从这里开始:
- 把 Safetensors 定为唯一允许的格式,例外必须说明理由。
- 每个模型按提交哈希固定,每个依赖按哈希锁定。
- 在模型进入注册表之前,先在 CI 里挂上一个扫描器。
- 默认禁止推理服务器的出站流量。模型服务器没有理由去访问未知主机。
- 让提示词、适配器和 MCP 服务器走与代码相同的评审流程。
来源:
想知道你们的模型和智能体供应链里到底有什么?我们聊聊。用 30 分钟,一起把这五道关卡对照你们的技术栈走一遍,没有任何义务。