前阵子技术社区里有个话题挺热闹:某 AI 公司为了测试自家模型的防护能力,模拟了一次针对 Hugging Face 的入侵演练,还主动“黑化”了自家模型。社区里不少人调侃“抱抱脸:?”,但笑过之后,这件事其实值得每个做 AI 应用开发的工程师停下来想一想。
Hugging Face 中文社区常被大家叫作“抱抱脸”,它是目前全球使用最广泛的模型托管与分享平台。很多团队直接从上面下载预训练模型,几行代码就接入业务。可问题在于:下载模型本质上是引入外部依赖,而且这种依赖比普通 npm 包或 pip 包更隐蔽,因为它不止包含代码,还包含权重文件、配置文件、分词器,甚至可能包含在加载阶段自动执行的脚本。
本文不打算凑热闹,而是把这件事背后的“模型供应链安全”完整拆一遍。你会看到恶意模型的常见攻击方式、为什么pickle格式风险极高、safetensors为什么更安全、trust_remote_code到底能不能开,以及一套从下载到加载的防御实操方案。
如果你平时用 transformers 做推理、微调,或者在公司内部搭建模型服务平台,这篇文章值得收藏备用。
1. 背景:模型供应链安全是什么
1.1 从一次“模拟入侵”说起
很多大模型公司会组建红队,专门模拟攻击自己的产品,以此找出漏洞。这次“模拟入侵 Hugging Face”的事件,本质上也属于红队测试范畴:攻击方不是真的搞破坏,而是验证“如果模型文件被恶意篡改,会造成多大影响”。
这件事之所以引发讨论,是因为它踩中了一个长期被忽视的痛点:大家太相信模型仓库里的文件了。
Hugging Face 上的模型仓库由贡献者上传,平台有基础审核,但无法保证每个文件都绝对安全。你下载的model.safetensors、pytorch_model.bin、config.json、tokenizer_config.json,甚至可能存在的.py脚本,都有可能是精心构造的恶意负载。
1.2 为什么开发者必须重视
很多后端工程师有良好的依赖管理习惯:package-lock.json、requirements.txt锁定版本,CI 里跑漏洞扫描。但到了模型下载这一步,却常常很随意。
举个例子:
git clone https://huggingface.co/some-org/some-model或者:
model = AutoModel.from_pretrained("some-org/some-model")这两行操作,背后隐藏着两层风险:
- 代码执行:如果仓库里有脚本文件,且加载时设置了
trust_remote_code=True,就会在本地执行任意 Python 代码。 - 反序列化漏洞:如果权重文件是
pickle格式,加载权重时可能触发任意代码执行。
也就是说,一个恶意模型仓库 = 一次远程代码执行机会。这在企业内网中尤其危险,因为开发机通常有较高的网络权限,可能直达数据库或内部管理系统。
1.3 几个核心概念先对齐
先统一几个术语,后面都会用到:
- 红队测试:模拟真实攻击者,用攻击手段探测系统弱点,目的是加固防御。
- 模型投毒:在训练数据、权重或模型结构中植入恶意逻辑,使模型在特定条件下输出异常结果。
- 供应链攻击:攻击者不直接攻击目标系统,而是污染上游依赖链,等目标主动拉取依赖时完成入侵。
- pickle 序列化:Python 的对象序列化协议,可以把对象转换为字节流,但它有个著名缺陷:反序列化时允许执行任意代码。
- safetensors:Hugging Face 主推的权重存储格式,只存张量数据,不执行代码,安全性远高于 pickle。
理解这些概念后,我们进入攻击面分析。
2. 攻击面分析:恶意模型有哪些入口
一个模型仓库里可以包含多种文件,每一种都可能成为攻击向量。下面按危险程度从高到低梳理。
2.1 恶意权重文件:pickle 的反序列化风险
这是最经典的攻击方式。PyTorch 的官方权重格式(.pt、.pth、.bin)基于pickle实现。pickle.load()在还原对象时,会执行对象里记录的__reduce__方法,从而允许构造恶意代码。
攻击者可以创建一个看似正常的权重文件,里面嵌入恶意负载。受害者加载模型权重时,恶意代码就在自己机器上执行了。
一个危险链路的示意如下:
下载模型仓库 → 加载 pytorch_model.bin → torch.load() → pickle 反序列化 → 执行恶意代码 → 机器被控制2.2 滥用 trust_remote_code 参数
transformers库允许模型仓库附带自定义 Python 代码。加载模型时设置:
AutoModel.from_pretrained("repo", trust_remote_code=True)模型仓库里的.py文件会被当成代码执行。这个机制本身是为了支持自定义模型结构,但问题在于:很多教程为了方便,直接建议打开trust_remote_code=True,却没有提醒用户检查仓库代码内容。
如果攻击者上传一个模型仓库,modeling.py里写一段反弹 Shell 或窃密逻辑,受害者只要加载模型就会中招。
2.3 恶意张量数据与 safetensors 绕过
safetensors格式本身是安全的,它只描述张量的字节偏移、形状和数据类型,不包含可执行逻辑。但攻击者依然可以利用:
- 在张量里隐藏数值后门,让模型在特定输入下输出异常结果;
- 构造超大文件消耗磁盘和内存,造成拒绝服务;
- 利用
safetensors解析库的历史漏洞发动攻击。
所以,“使用 safeftensors 就更安全”这句话要正确理解:它解决了代码执行问题,但没有解决模型功能层面的投毒问题。
2.4 数据集投毒与社会工程
除了权重文件,攻击者还可以污染训练数据集。比如上传一个“效果很好”的模型,训练数据里故意埋了 trigger,当输入包含特定图案或关键词时,模型输出被劫持。
另外,模型卡片(Model Card)里也可能写误导性说明,诱导开发者设置不安全的加载参数。这类攻击不依赖技术漏洞,而是利用信任链。
用一张表格总结攻击入口:
| 攻击入口 | 风险等级 | 触发条件 | 后果 |
|---|---|---|---|
| pickle 权重文件 | 极高 | torch.load()加载权重 | 任意代码执行 |
| 远程代码 | 高 | trust_remote_code=True | 任意代码执行 |
| 恶意张量 | 中 | 模型推理阶段 | 输出异常、隐私泄露 |
| 数据集投毒 | 中 | 微调或训练阶段 | 模型带后门 |
| 社会工程 | 低 | 人为误操作 | 安装恶意模型 |
3. 环境准备与实验边界
3.1 实验环境说明
本文后续的演示代码,建议在本地隔离环境运行。版本信息如下,你可以根据自己的环境调整:
操作系统:Ubuntu 22.04 / macOS / Windows WSL2 Python:3.9+ / 3.10+ / 3.11+ PyTorch:2.x transformers:4.x huggingface_hub:0.23+以上版本不是硬性要求,但如果你使用的是很旧的 transformers 或 huggingface_hub,部分 API 可能有差异,建议先升级到较新版本再实验。
3.2 安全边界声明
在进入演示之前,必须强调:本文所有攻击代码仅为教学演示,使用的是无害代码片段。请勿将本文内容用于攻击任何真实系统,请勿在未经授权的情况下扫描或入侵 Hugging Face 平台。
建议你按下面标准准备实验环境:
- 使用本地虚拟机或 Docker 容器,不要在公司内网机器上操作;
- 断网环境运行恶意负载示例;
- 运行完成后销毁容器;
- 生产环境禁止复制本文的攻击代码。
3.3 项目结构
我们创建一个实验项目,结构如下:
hack-hf-demo/ ├── malicious_repo/ # 模拟恶意模型仓库 │ ├── config.json │ ├── modeling.py │ └── model.safetensors ├── detector.py # 防御检测脚本 ├── safe_load.py # 安全加载示例 └── requirements.txt4. 红队视角:模拟恶意模型负载
这一节从攻击者视角拆解构造手法,但每一段都只给出原理示意,不提供可直接使用的完整恶意载荷。防御者只有理解攻击原理,才能正确配置防御策略。
4.1 Pickle 反序列化攻击原理
先看一个最简单的 pickle 恶意负载示意。
正常情况下,一个 PyTorch 模型保存后是 pickle 字节流。攻击者可以手动构造一个特殊对象,在__reduce__里指定反序列化时要执行的函数。
为了演示原理,我们用一个无害的print代替真正的恶意命令:
# 文件路径:malicious_repo/build_malicious_pickle.py # 注意:本文件仅用于教学演示,请勿用于非法用途 import pickle import builtins class Malicious: def __reduce__(self): # 反序列化时会执行 eval("print('pwned')") # 真实攻击中可能替换为 os.system 反弹 shell return (builtins.eval, ("print('pwned')",)) # 构造一个恶意 pickle 对象 payload = Malicious() with open("malicious.pt", "wb") as f: pickle.dump(payload, f) print("恶意 pickle 文件已生成:malicious.pt")当受害者使用torch.load("malicious.pt")加载这个文件时,pickle会调用__reduce__指定的方法,于是print('pwned')被执行。
真正的攻击中,攻击者会替换为:
import os os.system("bash -c 'reverse_shell_command'")或者下载并执行恶意脚本、遍历内网、读取环境变量中的密钥等等。
这里要澄清一个细节:很多人以为 PyTorch 的torch.load()为了安全会自动做校验,实际上除非显式指定weights_only=True,否则在旧版本中默认就是非安全的 pickle 加载。PyTorch 2.x 已经在逐步推进weights_only=True作为默认,但在较老版本里,风险是真实存在的。
所以防御第一原则已经出来了:
不要加载不信任的 pickle 格式权重文件。
4.2 trust_remote_code 恶意代码示例
transformers的远程代码机制为自定义架构提供了便利,但它也是代码执行的入口。看下面这个伪造的仓库文件:
# 文件路径:malicious_repo/modeling.py # 注意:此文件仅用于教学演示,请勿用于非法用途 import torch import torch.nn as nn class EvilModel(nn.Module): def __init__(self, config): super().__init__() self.linear = nn.Linear(10, 2) def forward(self, x): # 正常模型前向传播 return self.linear(x) # 模块被导入时执行下面的代码 # 真实攻击中可能替换为下载木马、读取环境变量等操作 print("恶意代码执行:模型文件被加载了!")当调用:
model = AutoModel.from_pretrained( "malicious_repo", trust_remote_code=True )加载过程中,transformers会从该仓库读取modeling.py并执行其中的顶层代码。上面的示例只打印一行字,但真实攻击中可以执行任意操作。
更可怕的是,攻击者可以让恶意逻辑看起来像正常功能。比如在modeling.py中定义一个窃取HF_TOKEN环境变量的函数,并把这个函数注册到模型的前向传播过程中,模型在推理时会把 token 发送到指定服务器。这种情况下,即使你审查了代码,也很容易忽略某个隐藏在长文件中的requests.post()调用。
4.3 从防御角度构造检测脚本
了解攻击原理后,我们可以写一个简单的本地检测工具,在下载模型后、加载模型前执行。
# 文件路径:detector.py # 功能:简单检测模型仓库中的高风险文件 import os import sys import json import pickle from pathlib import Path REPO_PATH = sys.argv[1] if len(sys.argv) > 1 else "malicious_repo" def scan_pickle_files(repo_path: str): """扫描 pickle 文件""" risky_exts = [".pt", ".pth", ".bin", ".ckpt", ".onnx"] findings = [] for ext in risky_exts: for p in Path(repo_path).rglob(f"*{ext}"): findings.append(p) return findings def scan_python_files(repo_path: str): """扫描仓库内 Python 文件""" return list(Path(repo_path).rglob("*.py")) def check_config(repo_path: str): """检查 config.json 中是否有自动加载代码的配置""" config_path = Path(repo_path) / "config.json" if not config_path.exists(): return [] with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) risky_keys = ["auto_map", "model_type", "custom_modeling"] return [k for k in risky_keys if k in config] def main(): print(f"开始扫描仓库:{REPO_PATH}") pickle_files = scan_pickle_files(REPO_PATH) py_files = scan_python_files(REPO_PATH) risky_config = check_config(REPO_PATH) if pickle_files: print("[高风险] 发现 pickle 权重文件:") for p in pickle_files: print(f" {p}") if py_files: print("[高风险] 发现 Python 脚本文件:") for p in py_files: print(f" {p}") print(" ⚠️ 如果加载模型时设置了 trust_remote_code=True,这些文件会被执行") if risky_config: print("[高风险] config.json 包含自动加载配置:") for key in risky_config: print(f" {key}") if not pickle_files and not py_files and not risky_config: print("未发现明显高风险文件,但仍需校验模型来源与仓库哈希。") if __name__ == "__main__": main()运行方式:
python detector.py malicious_repo这个脚本只是一个起点。实际工程中,还可以加入:
- 扫描 pickle 文件内部 opcode,判断是否包含
REDUCE、GLOBAL等高危指令; - 对 Python 文件做静态规则扫描,匹配
os.system、subprocess、socket、requests.post等高风险调用; - 结合 SAST 工具做更全面的代码审计。
5. 防御方案:如何安全使用 Hugging Face 模型
理解了攻击原理,防御就不是玄学了。下面给出可以直接落地的四层防御策略。
5.1 使用 safetensors 替代 pickle
首选方案是只允许加载safetensors格式的权重。
# 文件路径:safe_load.py # 功能:优先加载 safetensors 格式权重 from transformers import AutoModelForCausalLM, AutoTokenizer from huggingface_hub import snapshot_download # 下载模型到本地目录,只下载 safetensors 权重 repo_path = snapshot_download( repo_id="your-org/your-model", allow_patterns=["*.json", "*.safetensors", "*.txt", "*.md"] ) print(f"模型已下载到 {repo_path}")使用allow_patterns指定只下载safetensors与白名单文件,从源头上避开 pickle 文件。如果仓库里只有.bin权重,那么这个仓库就不应被允许加载。
也可以添加一个校验逻辑,检测下载目录中是否存在.bin、.pt等高危文件,发现则中断加载。
5.2 校验模型仓库哈希并锁定版本
Hugging Face 的仓库可以用 Git 管理,你可以锁定某个 commit 的哈希,而不是跟随“最新版”。
from huggingface_hub import snapshot_download # 固定 commit 哈希,确保每次拉取的代码一致 repo_path = snapshot_download( repo_id="your-org/your-model", revision="6f19c8a2b8a3d4c5e6f7890a1b2c3d4e5f6a7b8c9" )在 CI 或部署脚本中,应记录模型仓库的 commit 哈希和文件 sha256,验证通过后再加载。
# 下载后计算关键文件哈希 sha256sum model.safetensors在企业内部,更推荐把模型同步到私有对象存储或私有镜像仓库,业务侧从内网下载,减少对公网的直接依赖。
5.3 安全加载本地模型并禁用远程代码
无论仓库文件怎么变,加载侧可以做强约束:
# 文件路径:safe_load.py # 功能:安全加载配置示例 import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer local_dir = "/data/models/some-model" # 绝不开启 trust_remote_code model = AutoModelForSequenceClassification.from_pretrained( local_dir, trust_remote_code=False, # 显式关闭 use_safetensors=True, # 强制优先 safetensors torch_dtype=torch.float16 ) tokenizer = AutoTokenizer.from_pretrained(local_dir)注意use_safetensors=True这个参数:如果模型仓库同时包含.bin和.safetensors,transformers 会优先使用 safetensors。如果仓库只有.bin,则加载会报错,而不是默默退回 pickle 加载。
5.4 沙箱运行与最小权限
即使做了上述校验,也不能完全排除零日漏洞。在更高安全级别的环境中,建议:
- 把模型推理服务放在 Kubernetes 的独立 Pod 中;
- 容器以非 root 用户运行;
- 禁止容器访问内网敏感资源,只开放必要端口;
- 限制模型服务的出网白名单;
- 对模型加载目录开启只读挂载。
这样即使模型文件真的存在问题,恶意代码的影响范围也被控制在一个容器内。
6. 常见问题与排查思路
在实际操作中,读者遇到最多的几个问题整理如下。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
使用from_pretrained加载私有模型超时 | 内网无法访问 Hugging Face 公网 | 配置HF_ENDPOINT镜像,或先下载到内网 |
模型加载报错:Error no file named pytorch_model.bin | 仓库没有兼容的权重文件 | 检查仓库文件命名,或指定revision |
设置了use_safetensors=True仍加载失败 | 仓库缺少 safetensors 格式权重 | 改用use_safetensors=False,同时进行安全审查 |
| 加载模型时出现奇怪的打印信息 | 模型仓库代码被trust_remote_code=True执行 | 立即停用,检查仓库.py文件来源 |
| 推理时模型显存占用异常升高 | 可能是恶意张量或大型权重 | 监控显存峰值,核对模型参数大小 |
torch.load()报pickle.UnpicklingError | 文件损坏或被篡改 | 校验 sha256,从官方源重新下载 |
再补充两个高频场景的排查细节。
6.1 HF_ENDPOINT 配置不生效
如果你在企业内网使用镜像站,通常需要这样配置:
export HF_ENDPOINT=https://hf-mirror.com如果还是超时,请确认:
echo $HF_ENDPOINT检查环境变量是否在 Python 进程启动前导出。还有,某些版本需要同时设置:
import os os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"并且必须在导入transformers之前设置。
6.2 AutoModel 加载报错没有 modeling_xxx.py
如果一个模型仓库的config.json中指定了auto_map,就需要对应的自定义代码文件。当trust_remote_code=False时,加载会失败:
Loading the model requires you to trust remote code...此时不要盲目开启trust_remote_code。正确做法是:
- 下载整个仓库到本地;
- 人工审查
.py文件; - 确认无误后再使用
trust_remote_code=True; - 或直接把自定义代码复制到本地项目,改为本地加载。
7. 最佳实践与工程建议
7.1 建立模型准入清单
团队内部应该维护一份“可信模型清单”,包括:
- 模型仓库 ID;
- 固定 commit 哈希;
- 关键文件 sha256;
- 负责人;
- 最近安全审查日期。
任何新模型的接入,都必须走审批流程。这个流程可以接入 CI:
# 文件路径:.github/workflows/model-scan.yml name: Model Security Scan on: push: paths: - "models/**" jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: pip install -r requirements.txt - name: Run model scanner run: python detector.py models/your-model7.2 统一模型加载入口
不要把from_pretrained散落在各个业务代码中。建议封装一个统一的模型加载函数:
# 文件路径:model_loader.py import os import hashlib from pathlib import Path from transformers import AutoModelForCausalLM, AutoTokenizer ALLOWED_REPOS = { "your-org/your-model": { "revision": "6f19c8a2b8a3d4c5e6f7890a1b2c3d4e5f6a7b8c9", "sha256": "expected-sha256-of-model-file", } } class ModelLoadError(Exception): pass def verify_file_hash(filepath: str, expected_hash: str) -> bool: """计算文件 sha256 并与期望值比对""" sha256 = hashlib.sha256() with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): sha256.update(chunk) return sha256.hexdigest() == expected_hash def load_model_safe(repo_id: str, local_dir: str): """统一模型加载入口""" if repo_id not in ALLOWED_REPOS: raise ModelLoadError(f"模型 {repo_id} 不在白名单中") meta = ALLOWED_REPOS[repo_id] model_file = Path(local_dir) / "model.safetensors" if not model_file.exists(): raise ModelLoadError("模型文件不存在,请先下载") if not verify_file_hash(str(model_file), meta["sha256"]): raise ModelLoadError("模型文件哈希校验失败,可能被篡改") model = AutoModelForCausalLM.from_pretrained( local_dir, trust_remote_code=False, use_safetensors=True ) tokenizer = AutoTokenizer.from_pretrained(local_dir) return model, tokenizer封装后,业务侧只需要关心repo_id和local_dir,所有安全细节都收敛在加载层。
7.3 日志与审计
模型加载行为要记录日志,便于事后追溯:
- 谁在什么时间加载了哪个模型;
- 模型文件哈希是否通过校验;
- 是否开启了
trust_remote_code; - 是否替换过模型文件。
简单的日志格式:
import logging logger = logging.getLogger("model-security") logger.info("model loaded", extra={ "repo_id": repo_id, "revision": revision, "sha256": file_hash, "trust_remote_code": False, "user": current_user, })7.4 生产环境特别注意
- 不要在生产环境使用
from_pretrained直接访问 Hugging Face 公网,应该提前将模型下载到私有镜像仓库。 - 不要让模型服务容器拥有挂载宿主机根目录的权限。
- 不要在模型加载阶段泄露环境变量。如果你的镜像里有
HF_TOKEN、AWS_SECRET_ACCESS_KEY,恶意代码一旦运行,这些凭证就会外泄。 - 定期用工具扫描本地模型目录,发现
.pt、.pth、.bin文件就报警。
8. 学习路线
如果你想继续深入这个方向,建议按以下顺序学习:
- Python 序列化协议:读官方文档,理解
pickle的字节码结构,知道为什么__reduce__可以执行代码。 - PyTorch 模型保存机制:看
torch.save/torch.load源码,确认不同版本的weights_only参数行为。 - Hugging Face Hub 安全文档:阅读
huggingface_hub关于allow_patterns、revision、本地下载的说明。 - SAST 工具使用:学习 Semgrep 或 Bandit 的基本规则,尝试给自己的项目写自定义安全扫描规则。
- 容器安全基础:了解 Docker 只读根文件系统、capabilities、seccomp 等限制能力,把模型服务跑在最小权限容器里。
最后留一个建议:下次从 Hugging Face 下载模型前,先做三件事——
- 确认模型来自官方组织或可信作者;
- 只用
safetensors权重,不下载.bin、.pt文件; - 固定 commit 哈希,加载时关闭
trust_remote_code。
做到这三点,你已经避开了绝大多数模型供应链攻击。