
如果你是一名 AI 工程师打开 Hugging Face 搜索模型名复制一段from_pretrained代码然后直接加载进训练脚本——这套流程在近两年几乎成了肌肉记忆。但如果用安全工程的眼光重新审视整个过程会发现一个尴尬的事实你下载的不只是一堆权重数字而是一个可能被操作系统当作代码执行的二进制文件。2024 年初Hugging Face 平台与安全社区陆续披露了一批恶意模型。事件曝光后多数人的第一反应是“我只要不下载可疑模型就行”。但真正值得思考的不是那几个坏文件而是整个模型分发链条里“加载模型”这个动作一直没有被当作“执行不可信代码”来对待。本文不讨论具体攻击者身份而是从安全工程视角把这次事件拆开看看它暴露了哪些系统性问题以及一个开发团队可以用哪些低成本手段把风险降下来。文章会先讲清楚 Hugging Face 供应链的基本信任模型再复盘事件中的攻击链路然后用代码演示如何安全下载、校验和审计模型文件最后给出一套可落地的 AI 供应链安全最佳实践。如果你平时负责模型引入、训练平台建设或者算法工程的部署发布这篇文章值得读到底。1. 这篇文章真正要解决的问题很多人把 Hugging Face 安全事件理解成一个“个例”某个人上传了病毒平台清理了就行。但从安全工程的角度看这是一次典型的供应链投毒而且暴露的是结构性缺口。过去我们在 Java 项目里引入一个 Jar 包会依赖 Maven 仓库的校验机制、包管理器的依赖锁定、内部私服的代理和审计。Gradle 或 Maven 项目里至少有一个pom.xml或build.gradle去声明依赖。但模型引入呢大多数团队的做法是“谁需要模型谁自己下载”然后一个.bin文件就被直接加载进了数据分析师的 Jupyter Notebook甚至生产环境的推理容器。这里真正的风险不是“模型文件有病毒”这么简单而是整个流程缺少四个安全工程基本要素来源验证仓库里上传的模型是否真的来自可信的机构或作者。内容校验下载的文件是否和你预期的一致有没有被篡改。加载隔离即使文件有问题运行环境是否有能力把损害限制在最小范围。审计追溯哪些模型被谁、在什么时候、加载进了哪台机器。如果没有这些要素一次模型下载就可能演变成代码执行、凭据窃取、横向移动。这不是危言耸听而是 2024 年初那次事件里已经出现过的真实情况。2. Hugging Face 模型供应链的基本逻辑2.1 Hugging Face 是什么Hugging Face 是目前全球使用最广泛的机器学习模型和数据集托管平台。简单理解它就是 AI 领域的 GitHub开发者把训练好的模型权重、配置文件、tokenizer 字典和推理代码放到仓库里其他人通过huggingface_hub库或网页直接下载使用。这类平台的价值在于标准化。它统一了模型文件的存储格式、版本管理和分享方式让“下载模型”变成一条命令行就能完成的事情。但也正是因为这种便利它成了供应链攻击的高价值目标。2.2 AI 模型供应链与传统软件供应链的差异很多安全工程师第一次接触模型供应链时会下意识把它等同于“代码仓库 依赖管理”。但这两者有一个关键差异传统软件依赖的二进制包也有执行风险但开发者通常知道“这个 Jar 包会被加载进 JVM 并执行字节码”而模型权重文件在开发者心智里是“数据”很少有人意识到它也会触发代码执行。对比维度传统软件供应链AI 模型供应链主要交付物源码、依赖包、容器镜像模型权重、数据集、配置文件是否可执行明确知道会执行容易误以为只是数据校验手段包管理器签名、锁文件、SBOM平台审核、仓库哈希落地校验较少运行时隔离容器、沙箱、低权限账号多数直接加载进训练或推理进程资产管理有成熟的依赖审计工具模型资产台账普遍缺失可以看到AI 模型供应链在很多方面还处于“早期阶段”。这不是批评 Hugging Face 一家平台而是整个生态的共同问题。2.3 为什么 Hugging Face 会成为攻击目标从攻击者的视角看Hugging Face 有三个天然优势第一是流量大。高质量的预训练模型会被大量开发者下载一次成功的投毒就能覆盖很多目标。第二是信任惯性。开发者默认仓库页面上标星的模型、官方组织的模型是可信的这种信任很容易被利用。第三是执行面广。模型文件在加载时经过的解析链路非常复杂从压缩包解压、反序列化到动态加载配置每一环都可能藏攻击载荷。所以对一个安全团队来说问题不是“Hugging Face 安不安全”而是“我们能不能在自己的网络边界内验证和约束每一次模型加载行为”。3. 事件复盘从“下载了恶意文件”到“加载即执行”3.1 公开事件的核心事实根据公开披露的信息2024 年初 Hugging Face 官方和安全社区清理了数十个包含恶意代码的模型和数据集。这些模型表面上是正常的 AI 模型有完整的模型卡、正常的版本号但权重文件或附加脚本里被植入了恶意逻辑。其中一种典型手法是在 PyTorch 权重文件中嵌入恶意 pickle 操作码。当开发者使用torch.load()加载这个文件时恶意代码就在开发者的机器上执行了。大家可以想象一下如果这台机器恰好是一台带有训练数据的 GPU 服务器攻击者能拿到什么。3.2 攻击链路的工程拆解把一次模型投毒攻击拆开可以分为四个步骤制作恶意模型攻击者先下载一个正常模型用修改后的权重文件替换原有文件并在其中嵌入恶意载荷。上传到平台给模型起一个和热门模型高度相似的名字补全模型卡、标签和 README让它看起来可信。诱导下载通过社区推广、搜索引擎优化或伪装成“效果好、下载量高”的模型等待开发者上钩。触发执行开发者在本地执行from_pretrained或torch.load恶意代码在反序列化阶段运行。这里的关键点在于第 4 步。很多开发者的直觉是“只要我不运行可执行文件就不会中毒”。但模型文件的反序列化过程本身就是执行过程它不需要你额外点击任何东西。3.3 Pickle 反序列化为什么会成为突破点Pickle 是 Python 内置的对象序列化协议。它可以把 Python 对象转换成字节流也可以从字节流恢复对象。问题在于pickle.loads()在恢复对象的过程中可以执行任意 Python 函数和对象构造。PyTorch 早期版本的权重保存格式基于 pickle所以加载 PyTorch 模型权重时本质上是在做一次不可信的反序列化。一个精心构造的 pickle 文件可以在torch.load()返回权重张量之前先执行os.system()或导入一个恶意模块。这就是为什么安全社区一直推动使用 safetensors 格式safetensors 是纯数据格式只保存张量数据不包含可执行逻辑。从安全工程的角度看它把“加载模型”从“执行代码”变回了“读取数据”。3.4 safetensors 为什么更安全safetensors 由 Hugging Face 团队设计目标就是替代基于 pickle 的权重存储。它的核心设计是文件头部用 JSON 描述张量的元信息主体部分按字节偏移记录原始张量数据整个解析过程不涉及对象反序列化。对比一下两种加载方式# pickle 方式存在反序列化执行风险 weights torch.load(pytorch_model.bin, map_locationcpu) # safetensors 方式纯数据解析 from safetensors.torch import load_file weights load_file(model.safetensors)这不是说用了 safetensors 就万事大吉而是说它移除了攻击面最大的那一层。就像 Web 安全里把不可信输入和 SQL 语句做参数化分离safetensors 相当于把“数据”和“代码”在格式层面分开了。4. 开发者日常流程里的信任缺口4.1 一个普通加载流程的信任假设假设你在项目里写下这一行代码from transformers import AutoModel model AutoModel.from_pretrained(some-org/some-model)这行代码背后至少发生了四件事从 Hugging Face 下载模型目录下的权重文件、config.json、tokenizer 文件。解析 config.json决定模型结构。按需加载权重文件到内存。如果trust_remote_codeTrue还会执行仓库里的自定义 Python 代码。每一步都有各自的信任假设。config.json 是 JSON 文件相对安全权重文件如果是 pickle 格式加载时可能执行任意代码自定义模型代码一旦运行风险等同于运行一个未知的 Python 脚本。但对大多数开发者来说这些过程是封装在from_pretrained里的看不见也就不会意识到风险。4.2 容易被忽略的注入入口很多人以为只有.pt和.bin文件才有风险其实攻击者可以选择的入口比想象中多PyTorch 权重文件.pt、.bin、.pth都可能是 pickle 格式。数据集目录数据集中可能混入.pt、.pkl文件在训练脚本遍历文件时被torch.load()加载。tokenizer 或配置文件某些库在解析特制配置时会引入额外逻辑。自定义模型代码配合trust_remote_codeTrue的仓库任意 Python 代码都会被执行。压缩包如果模型以 archive 方式分发解压过程本身的漏洞也是一种入口。这里真正容易踩坑的地方是数据集。很多团队对模型文件有警觉但训练脚本经常会把数据集目录下的所有文件自动加载一旦数据集里混入一个恶意.pkl文件就会被批量处理。4.3 安全工程视角的三个缺失从事件复盘来看整个生态缺失最明显的三件事是签名验证、加载隔离和运行时审计。签名验证解决“这个模型是不是某个人/某机构发布的原始文件”的问题加载隔离解决“即使模型有问题攻击能影响的范围有多大”的问题运行时审计解决“谁在什么时候加载了什么模型”的问题。这三件事在传统软件供应链里都有成熟实践但模型供应链目前还在逐步补齐。这也是留给工程团队的机会谁先把这些实践落地谁就能在后续的 AI 生产中占据安全优势。5. 实操安全获取与加载 Hugging Face 模型5.1 环境准备与安装本文的示例基于 Python 3.9需要安装huggingface_hub和transformers。建议在虚拟环境中操作不要直接装到系统 Python 里。python -m venv .venv source .venv/bin/activate pip install --upgrade huggingface_hub transformers safetensorsHugging Face 官方库会从平台下载模型文件。如果你所在的企业内网无法直接访问外网模型仓库更稳妥的方式是在一个有完整网络的环境里下载模型到本地目录并校验哈希然后通过内部制品库或离线介质导入到生产内网。导入后重新计算哈希并登记不要直接使用来源不明的“加速包”。5.2 使用 huggingface_hub 下载并校验hf_hub_download是huggingface_hub提供的下载接口它会把模型文件下载到本地缓存或指定目录。下面是下载一个模型文件的示例# 文件路径download_model.py from huggingface_hub import hf_hub_download repo_id bert-base-uncased filename pytorch_model.bin local_path hf_hub_download( repo_idrepo_id, filenamefilename, local_dir./models/bert-base-uncased, ) print(下载完成:, local_path)如果只想下载一个模型的完整目录可以用snapshot_downloadfrom huggingface_hub import snapshot_download snapshot_download( repo_idbert-base-uncased, local_dir./models/bert-base-uncased, )下载完成后建议立刻计算文件哈希并和仓库页面中的 LFS 元数据比对。Hugging Face 对超过一定大小的文件会使用 Git LFS 管理LFS 文件通常有对应的 SHA256 记录。你可以在本地对比sha256sum ./models/bert-base-uncased/pytorch_model.bin这一步的意义在于你能确认自己拿到的文件和仓库记录一致而不是被中途替换过的版本。对安全要求高的场景可以同时比对 commit hash锁定模型版本。5.3 加载前检查文件类型下载完成后先不要急着加载。一个简单的习惯是列出模型目录下的所有文件检查是否有不应该出现的文件类型。find ./models/bert-base-uncased -type f | sort正常情况下一个 PyTorch BERT 模型目录应该包含权重文件、config.json、vocab.txt 或 tokenizer.json。如果目录里出现了.py、.sh、.so、.dll、.exe等文件就需要高度警惕。更规范的做法是用脚本扫描可疑扩展名这个脚本我会在下一章给出。5.4 使用 transformers 时的安全参数transformers的from_pretrained方法里有一个经常被忽略的参数trust_remote_code。from transformers import AutoModel, AutoTokenizer # 安全做法不执行仓库里的自定义代码 model AutoModel.from_pretrained( ./models/bert-base-uncased, local_files_onlyTrue, trust_remote_codeFalse, ) tokenizer AutoTokenizer.from_pretrained( ./models/bert-base-uncased, local_files_onlyTrue, trust_remote_codeFalse, )trust_remote_codeFalse是 transformers 的默认值它表示不加载模型仓库里的自定义 Python 代码。有些模型需要自定义代码才能运行这时候你就要想清楚你是否信任这个仓库的作者如果你必须加载一个使用了自定义代码的模型建议先审查仓库里的modeling_*.py文件而不是直接信任。local_files_onlyTrue只从本地目录加载文件不会再次联网这适合已经下载并登记过的模型。6. 用最小代码做模型安全基线审计这一章提供的脚本不是用来替代专业安全工具而是给算法工程师一个“加载前先扫一眼”的基线动作。三个脚本分别解决目录里有什么、pickle 文件里引用了什么、加载时如何限制执行。6.1 审计模型目录中的可疑文件# 文件路径audit_model_dir.py from pathlib import Path def audit_model_dir(model_dir: str): suspicious_exts { .pt, .pkl, .pickle, .pth, .npy, .npz, .joblib, } result [] for p in Path(model_dir).rglob(*): if not p.is_file(): continue if p.suffix.lower() in suspicious_exts: result.append(str(p)) return result if __name__ __main__: found audit_model_dir(./models/bert-base-uncased) if found: print([!] 发现以下 pickle/序列化文件) for f in found: print( , f) else: print([] 目录中没有发现可疑序列化文件)这段代码的作用是“盘点”。很多安全事件都是因为开发者根本不知道自己下载的目录里有哪些文件。先盘点再做判断顺序很重要。6.2 审计 Pickle 文件的可疑操作码如果你确定某个文件就是 pickle 格式可以用 Python 自带的pickletools解析它检查反序列化过程中会引用哪些模块和函数。# 文件路径audit_pickle.py import io import pickletools from pathlib import Path def audit_pickle(filepath: str): data Path(filepath).read_bytes() print(f正在审计: {filepath} (大小: {len(data)} bytes)) print(被解引用的 GLOBAL 操作码) for opcode, arg, pos in pickletools.genops(io.BytesIO(data)): if opcode.name in (GLOBAL, STACK_GLOBAL, REDUCE, INST): if arg: print(f opcode{opcode.name}, arg{arg}, offset{pos}) if __name__ __main__: audit_pickle(./models/bert-base-uncased/pytorch_model.bin)需要提醒的是这个脚本不等于“安全判定”。一个正常的 PyTorch 权重文件也会大量引用torch._utils、collections.OrderedDict等模块。所以不要看到输出就紧张关键是观察是否有os、subprocess、socket、eval、exec、__import__等与系统交互的模块被引用。如果看到这类内容这个模型就有重大嫌疑。6.3 用 weights_only 限制反序列化行为新版 PyTorch 在torch.load()中提供了weights_only参数。设置为True时反序列化只允许加载张量等安全类型不执行任意对象构造。这是当前最容易执行的加固手段之一。import torch # 旧代码默认加载方式 # weights torch.load(pytorch_model.bin, map_locationcpu) # 加固代码只允许加载张量 weights torch.load( pytorch_model.bin, map_locationcpu, weights_onlyTrue, )如果你的 PyTorch 版本支持这个参数建议显式开启。如果遇到“不支持的全局对象”之类的报错说明这个权重文件里包含了普通张量之外的内容需要停下来检查文件来源。6.4 哈希校验脚本与离线导入对于需要长期复用、频繁加载的模型建议把哈希校验固化到加载流程里。下面是一个简单实现# 文件路径verify_sha256.py import hashlib from pathlib import Path def sha256_of(path: Path) - str: h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(4096), b): h.update(chunk) return h.hexdigest() if __name__ __main__: path Path(./models/bert-base-uncased/pytorch_model.bin) expected # 在模型登记表里记录这个值 actual sha256_of(path) print(实际 SHA256:, actual) if expected and actual ! expected: raise SystemExit(哈希不匹配文件可能已被篡改)实际工程中可以把模型文件、哈希值和来源信息登记到内部资产系统。每次加载前先校验校验通过才允许进入推理或训练流程。7. 常见问题与排查思路问题现象可能原因排查方式解决方案下载的模型加载时报错文件不完整或哈希不一致重新计算 SHA256与仓库记录比对重新下载或从内部制品库获取weights_onlyTrue加载报错权重文件包含非张量对象检查文件来源和信息改用 safetensors 版本或审查来源目录中出现了.py/.sh文件模型仓库包含自定义代码检查模型卡和仓库内容不要直接运行审查代码后再决定from_pretrained执行了仓库代码trust_remote_codeTrue检查调用代码和加载日志改为False或人工审查自定义代码内网无法访问模型仓库网络策略限制外网检查网络连通性和代理配置在可控环境下载后通过内部制品库导入训练时误加载数据目录中的.pkl加载逻辑遍历了目录全部文件查看训练脚本中的文件发现逻辑白名单扩展名只加载预期格式的文件不知道模型文件来自谁缺少来源登记查模型卡的 owner 和上传时间建立模型引入审批和登记流程这里最容易被忽略的是最后一个问题很多团队用着大量模型却说不清这些模型最早是从哪里下载的、谁负责引入的、文件哈希是什么。这件事一旦等到事件发生再来补代价就大了。8. AI 供应链安全工程最佳实践8.1 纵深防御不要把安全希望寄托在“这一次的模型没问题”上。纵深防御的意思是每一层都不完全可信需要多层校验。一个推荐的分层模型是下载层只从可信来源下载记录来源和哈希。验证层加载前校验哈希、文件类型、pickle 引用清单。运行层使用容器或沙箱隔离加载过程限制网络和文件系统访问。监控层记录模型加载的账号、时间、机器和文件路径异常时能追溯。每一层都不追求绝对完美但叠加起来能大幅抬高攻击成本。8.2 最小权限在模型加载机器上使用专用低权限账号禁止使用 root 或管理员账号运行推理脚本。容器运行时可以进一步限制不挂载敏感目录。不开放出网权限。不把生产数据库凭据注入到模型服务的环境变量里。如果攻击者通过模型文件拿到了代码执行权限这些限制能避免他从“模型投毒”直接升级成“数据库拖库”。8.3 验证与审计模型文件的验证和审计要尽量自动化。最小可行方案是做一个模型引入登记表包含以下字段模型名称和版本。来源仓库地址。引入人、引入时间和使用场景。文件哈希。运行时是否使用自定义代码。是否通过安全审查。这个登记表可以作为模型资产台账后续每次加载模型时都能对照检查。8.4 团队流程与生产环境注意事项生产环境引入模型时建议走一个轻量审批流程。不用搞得很重但至少要有一个人确认“这个模型的来源是可信的哈希已登记”。对于涉及敏感数据的团队优先选择 safetensors 格式的模型避免 pickle 权重。推理服务上线时尽量把模型文件和模型服务代码一起打包成容器镜像镜像构建时完成哈希校验。不要在容器启动时临时从外部下载模型这既影响可重复构建也扩大了攻击面。9. 总结与后续学习方向这次 Hugging Face 安全事件带来的真正启示不是“要小心下载来源”而是“加载模型应当被当作执行不可信代码来看待”。模型文件不是普通的数据文件它的反序列化、解析和自定义代码加载过程都存在执行面。安全工程要做的事情是把这些执行面一个一个收窄。对于日常开发可以从三件事开始改进下载模型后记录哈希加载前扫描文件类型加载时使用 safetensors 或weights_onlyTrue。对于平台团队则需要思考模型仓库的权限控制、内部制品库、资产登记和加载审计。这些能力不需要一步到位但每多一层攻击成本就高一分。如果你想继续深入可以关注几个方向safetensors 的格式规范和解析细节、AI 模型的指纹与水印技术、TEE 可信执行环境在模型推理中的应用以及 AI 红队针对模型供应链的攻防演练。这些话题比“某一次事件”更值得长期投入也更能回答一个问题当模型供应链里的“数据”和“代码”边界越来越模糊时安全工程的防线应该如何重新划定。