ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

模型加载安全:pickle反序列化与trust_remote_code风险

2026/8/28 14:34:05 拓冰建站 浏览量
模型加载安全:pickle反序列化与trust_remote_code风险 先把一个常见场景放在这里你从 Hugging Face 找到一个 embedding 模型看名字应该是某大厂出品下载量不小评分也可以。于是你在自己的服务器上写了这样一行代码from transformers import AutoModel model AutoModel.from_pretrained(some-creator/embedding-model)大多数开发者不会去追查这行代码背后发生了什么。它看起来只是在下载权重、加载模型、开始推理。但实际情况是这行代码可能触发 pickle 反序列化可能执行模型仓库里的远程代码也可能让你的服务器在一分钟内被攻击者控制。这不是理论推演。近期美国阿拉巴马州向 OpenAI 发出传票调查一起与 Hugging Face 模型相关的入侵事件。案件细节还没有完全公开但围绕“模型 代码执行 托管平台”的供应链安全问题已经摆在了每个普通开发者面前。这篇文章不会替任何一方做事实认定而是从技术角度拆解这类事件为什么会发生以及开发者到底应该怎么保护自己。你会看到模型为什么能成为攻击入口pickle 和 safetensors 的本质区别是什么并且拿到一套可以直接落地的模型安全下载、扫描、隔离运行流程。就算你目前只做业务开发不研究模型底层这套流程同样值得收藏。1. 这次调查背后真正值得关注的是什么1.1 事件的基本信息根据公开报道美国阿拉巴马州方面已经向 OpenAI 发出传票调查方向与 Hugging Face 上发生的模型相关入侵事件有关。具体是哪个模型、哪次攻击、影响范围有多大目前仍缺乏官方细节。这里需要给读者一个清晰判断这大概率不是一次“某个 AI 工具的 API 被攻破”的简单事件。从技术社区关注点来看它折射的是 AI 模型供应链的安全责任问题。OpenAI 是模型生产方Hugging Face 是模型分发平台而最终加载模型的是千千万万开发者的服务器。三者之间的信任链一旦被恶意利用事件责任边界会非常模糊。1.2 为什么 Hugging Face 会成为焦点Hugging Face 已经是 AI 开发者绕不开的站点。每天有大量开发者从这里下载模型而它的开放模式决定了任何人注册后都能上传模型仓库附带任意文件。平台有安全扫描机制也接受恶意样本举报但在海量模型面前自动扫描并不能保证覆盖所有恶意变体。攻击者并不需要正面攻破平台他们只需要把恶意文件塞进一个看起来正常的模型仓库。模型是合法的权重是真的但某个配置文件里藏了一段反序列化攻击代码。这种攻击方式比直接入侵服务器成本低得多效果却可能更隐蔽。1.3 为什么 OpenAI 会被卷入模型与代码执行链路OpenAI 之所以和这件事扯上关系除了它本身就是最大的模型厂商之一更值得关注的是它的工具正在把“模型”和“本地代码执行”紧紧连在一起。OpenAI 开源的 Codex CLI 可以在开发者电脑上执行命令、操作文件、调用各种工具因此也成为一个新的攻击落点。可以这么理解在过去下载模型是一个相对独立的行为最多影响进程内推理结果。但到了 Codex 这类智能体工具普及之后模型下载、本地执行、文件操作、网络访问形成了一条完整链路。攻击者只要在链路中的某一个环节植入恶意代码就有机会拿到比模型本身大得多的权限。这也是为什么监管机构开始关注模型分发环节。2. 模型为什么会成为攻击入口2.1 模型文件不只是数据很多开发者把模型权重当成普通数据文件这是一个核心误区。PyTorch 的权重文件、TensorFlow 的 checkpoint、甚至 Hugging Face 上常见的.bin、.pt、.pkl文件本质上都是序列化数据。而 PyTorch 默认使用的序列化格式是 pickle。pickle 不是简单的数据格式。它是 Python 内置的对象序列化协议反序列化时允许执行任意代码。也就是说你加载一个模型权重文件可能不只是读了几个浮点数而是启动了一个隐藏的脚本。官方文档和大量安全报告都在强调这一点永远不要反序列化不可信数据。2.2 pickle 反序列化的风险当 transformers 库加载一个老式 PyTorch 权重文件时底层会调用torch.load。默认情况下这个过程会使用 pickle 解析文件内容。如果攻击者构造了一个恶意权重文件其中某些对象在反序列化时会触发__reduce__就会执行系统命令。社区里著名的“big pickle”风险演示项目专门用了一个大体积 pickle 文件来证明打开一个模型文件等于运行一个未知程序。它可能只是让你机器卡顿也可能反弹一个 shell。这是真实存在多年的风险只是大多数应用代码把它封装得太隐蔽以至于开发者已经完全无感。2.3 safetensors 能解决什么Hugging Face 后来推出了 safetensors 格式核心目标就是消除 pickle 风险。safetensors 只保存张量数据不包含可执行对象反序列化过程非常直接不会运行任何代码。所以现在官方强烈建议模型发布方使用 safetensors也建议使用方显式加载这个格式。当然safetensors 不是万能的。一个模型仓库里可以同时存在.safetensors和.bin文件如果代码里写死加载.bin风险仍然存在。此外模型仓库里还可能有config.py、custom_model.py、preprocessor_config.json等代码文件加载逻辑稍不注意就会执行。2.4 trust_remote_code 的隐性风险transformers 加载模型时有一个参数叫trust_remote_code。很多模型仓库会在配置中声明自定义代码要求使用者开启这个参数才能正常加载。一旦开启框架会从模型仓库下载并执行自定义 Python 代码。对开发者来说这几乎是主动关闭了安全防线。trust_remote_codeTrue可以解决兼容性问题但代价是模型作者可以在你的机器上运行任意代码。社区已经多次报告过伪装成知名模型、诱导用户开启该参数的恶意仓库。因此除非模型来源绝对可信否则不要轻易开启。下面用表格对比几种常见模型文件格式的安全性格式是否可能执行代码使用场景安全建议.bin/.pt/.pth是走 pickle 反序列化老式 PyTorch 权重避免直接从不可信来源加载.safetensors否仅存储张量Hugging Face 推荐的权重格式优先选择此格式.gguf否llama.cpp 专有格式本地推理、量化部署仍需校验来源与哈希.py/.json自定义代码是取决于加载逻辑自定义模型结构非必要不开启 trust_remote_code3. 典型攻击链路拆解从下载到失控这一节从攻击者的视角拆解一次完整的模型供应链攻击过程。请注意这里只是安全分析不是攻击教程。理解链路是为了知道该在哪些环节设防。3.1 阶段一投放恶意模型攻击者会注册一个与知名模型高度相似的账号或仓库名比如把google-bert/bert-base-uncased改成google-berts/bert-base-uncased或者使用qwen3.5-9b-gguf这类蹭热词的名称。模型卡片写得很专业下载数用脚本刷到几千很多人就会放松警惕。这类恶意仓库通常不是“假模型”而是把真实模型权重和恶意配置文件混在一起。权重部分可以正常工作开发者跑几个测试用例发现效果没问题于是彻底失去戒心。3.2 阶段二本地加载触发代码执行开发者在服务器上执行from_pretrained后框架会先读取config.json根据配置决定要不要加载自定义代码然后再加载权重。如果权重是.bin格式pickle 反序列化会在加载那一刻执行隐藏命令。攻击者往往不直接做明显破坏而是植入反弹 shell、下载后续恶意负载、添加 SSH 公钥、创建隐藏定时任务。这些行为在外观上不会立刻影响模型推理因此很难被发现。3.3 阶段三横向移动一旦服务器被控制攻击者会观察这台机器的网络环境。如果它只是开发机攻击者可以把它变成跳板尝试访问内网数据库、缓存服务、CI 系统。很多企业内网对开发机放得比较宽这给了横向移动很大空间。更隐蔽的一种做法是“模型投毒”攻击者不直接夺取服务器而是修改模型输出在某些特定输入下返回恶意结果。这在风控、内容安全、自动化决策等场景下可能造成更大的业务损失。3.4 阶段四持续混淆恶意模型在云服务器上往往不会立即暴露。攻击者会尽量把行为伪装成正常服务例如定时向远程服务器发送带 token 的 HTTP 请求。直到有安全团队检查网络流量、审计文件完整性、扫描模型文件时才可能发现问题。如果团队同时在使用 OpenAI Codex 这类具备本地代码执行能力的智能体工具风险传播面会更大。Codex 可以访问当前目录、运行 shell 命令也就可能成为模型供应链攻击的放大器。4. 安全使用 Hugging Face 模型一套可落地的防护流程面对这类风险最有效的策略不是因噎废食而是建立一套标准的模型接入流程。下面这套流程从“下载前”到“运行后”都给出了对应的动作。4.1 环境准备建议在独立的 Python 虚拟环境中操作避免污染系统环境。需要安装的依赖python -m venv .venv source .venv/bin/activate pip install --upgrade pip huggingface_hub transformers safetensors如果你的项目需要使用 PyTorch请根据官方文档安装合适版本。这里不限定具体版本号以你项目实际依赖为准。4.2 下载前的模型档案审查不要只凭模型名称和下载量做判断至少检查以下几个维度检查项说明发布者身份是否为官方机构账号是否经过组织认证模型卡片是否包含训练数据、用途、限制说明是否像复制粘贴的模板文件列表是否存在.py、.pth、.bin、.pkl等可疑文件下载历史下载量波动是否异常是否有大量短时间重复下载社区讨论Issues 和 Discussions 里是否有人反馈安全或兼容性问题下载时也可以指定版本号不要直接跟随默认分支避免模型作者在更新中注入恶意代码huggingface-cli download your-org/your-model --revision sha256_or_tag --local-dir ./safe-model固定 commit 是重要的一步。任何人重新上传同名文件commit 都会变化因此固定版本可以防止“同一仓库内容被悄悄替换”。4.3 文件级体检扫描扩展名与可疑内容下载完成后不要急着加载先用脚本扫描模型目录。重点检查是否存在.bin、.pt、.pth、.pkl、.joblib文件是否存在.py文件尤其是custom_model.py、modeling_xxx.py是否存在隐藏文件、加密压缩包、shell 脚本是否存在异常大的 JSON 文件因为 JSON 里也可能夹带长字符串。下面是一个简单的安全扫描脚本你可以保存到项目根目录使用import os import zipfile import tarfile from pathlib import Path SUSPICIOUS_EXTS { .pt, .pth, .bin, .pkl, .joblib, .pickle, .npy, .npz, .py, .pyc, .sh, .bat, .ps1, .so, .dll, .dylib, } def _is_suspicious(name: str) - bool: lower name.lower() return Path(lower).suffix in SUSPICIOUS_EXTS or any( kw in lower for kw in [shell, reverse, payload, exploit, backdoor] ) def scan_directory(root: str): root_path Path(root) if not root_path.exists(): print(f[ERROR] 路径不存在: {root}) return print(f[*] 开始扫描目录: {root_path.resolve()}) suspicious_files [] for dirpath, _, filenames in os.walk(root_path): for filename in filenames: full_path Path(dirpath) / filename rel_path full_path.relative_to(root_path) if _is_suspicious(str(rel_path)): suspicious_files.append(str(rel_path)) print(f[!] 疑似可疑文件: {rel_path}) # 检查压缩包内文件 if filename.endswith(.zip): try: with zipfile.ZipFile(full_path) as zf: for name in zf.namelist(): if _is_suspicious(name): suspicious_files.append(f{rel_path} - {name}) print(f[!] 压缩包内发现可疑文件: {rel_path} - {name}) except zipfile.BadZipFile: print(f[!] 损坏的 zip 文件: {rel_path}) if filename.endswith(.tar) or filename.endswith((.tar.gz, .tgz)): try: with tarfile.open(full_path) as tf: for member in tf.getmembers(): if _is_suspicious(member.name): suspicious_files.append(f{rel_path} - {member.name}) print(f[!] 压缩包内发现可疑文件: {rel_path} - {member.name}) except tarfile.TarError: print(f[!] 损坏的 tar 文件: {rel_path}) if not suspicious_files: print([*] 未发现明显可疑文件可以进入下一步验证。) else: print(f[!] 发现 {len(suspicious_files)} 个可疑文件请人工确认后再决定是否使用。) if __name__ __main__: scan_directory(./downloaded-model)脚本的逻辑很简单按扩展名和关键词过滤文件同时递归检查压缩包。它不能发现所有恶意样本但对于大多数“模型里藏文件”的可疑情况已经足够。4.4 优先使用 safetensors 加载如果模型仓库同时提供.safetensors权重和.bin权重应该显式指定从 safetensors 加载。transformers 在from_pretrained时会优先寻找 safetensors但这不是绝对的。为了保证安全可以先确认本地确实存在 safetensors 文件然后检查加载日志确保没有走到 pickle 路径。如果只有.bin权重可以通过转换工具把权重转成 safetensors再进行加载。转换公式并不复杂python -m safetensors.torch --help至少在企业内部使用场景中建议所有模型统一转成 safetensors 后再入库不接受原始 pickle 权重直接上线。4.5 使用容器和沙箱验证在正式接入业务之前建议先把模型放到一个隔离环境里试运行例如 Docker 容器。容器必须限制网络使用--network none这样可以阻断恶意代码的远程通信。docker run --rm -it --network none -v $PWD/downloaded-model:/models python:3.11-slim bash容器内可以先执行一个简单的加载测试。如果模型加载过程会长时间卡住、意外调用外部端口、生成异常文件基本可以判定模型有问题。即使模型被确认安全业务上线时也应该保持最小权限运行容器只挂载模型目录只开放必要端口。4.6 加速下载的注意事项国内开发者在 Hugging Face 下载模型时经常遇到速度慢的问题。可以选择通过公开镜像站加速例如设置环境变量指向hf-mirror.comexport HF_ENDPOINThttps://hf-mirror.com需要说明的是镜像站只负责加速下载不改变模型内容的真实性。无论从哪里下载都必须做哈希校验。加载前对比模型仓库页面提供的 commit hash 或文件哈希确保本地文件与远端完全一致。5. 完整示例代码与运行验证为了让你能直接照着操作这一节给出一个更完整的最小闭环。假设我们要使用一个虚构的模型仓库your-team/embedding-model。5.1 安全下载脚本新建download_model.pyfrom huggingface_hub import snapshot_download import hashlib from pathlib import Path REPO_ID your-team/embedding-model LOCAL_DIR ./downloaded-model def sha256_file(path: Path, chunk_size: int 8192) - str: h hashlib.sha256() with open(path, rb) as f: while chunk : f.read(chunk_size): h.update(chunk) return h.hexdigest() def main(): print(f[*] 开始下载模型: {REPO_ID}) snapshot_download( repo_idREPO_ID, local_dirLOCAL_DIR, local_dir_use_symlinksFalse, resume_downloadTrue, ) model_path Path(LOCAL_DIR) print(f[*] 模型已下载到: {model_path.resolve()}) # 对关键文件做哈希 for name in [config.json, model.safetensors, tokenizer.json]: file_path model_path / name if file_path.exists(): digest sha256_file(file_path) print(f[HASH] {name}: {digest}) if __name__ __main__: main()执行python download_model.py下载完成后脚本会输出关键文件的 SHA-256 值。把这些值与模型仓库页面或官方发布文档中的哈希值对比一致才能进入下一步。5.2 加载前扫描把前面第 4.3 节的扫描脚本保存为scan_model.py然后执行python scan_model.py ./downloaded-model如果发现.bin、.pkl等文件优先人工确认。如果发现.py文件需要认真阅读源码确认没有可疑的网络请求、文件删除、进程执行等操作。5.3 仅从 safetensors 加载模型新建load_model.pyfrom transformers import AutoModel, AutoTokenizer MODEL_DIR ./downloaded-model # 明确使用 safetensors 加载并在加载前检查文件存在性 import os assert os.path.exists(os.path.join(MODEL_DIR, model.safetensors)), 未找到 safetensors 权重 tokenizer AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_codeFalse) model AutoModel.from_pretrained(MODEL_DIR, trust_remote_codeFalse) print([*] 模型加载成功)这里的关键点是trust_remote_codeFalse。即使模型配置里声明需要自定义代码我们也不在初次验证时开启。如果模型完全依赖自定义代码运行那就需要人工审查代码之后再单独决定是否放开。5.4 Docker 隔离运行如果你希望再增加一层隔离可以把模型加载过程放进容器docker run --rm -it --network none \ -v $PWD/downloaded-model:/models:ro \ -v $PWD/load_model.py:/app/load_model.py:ro \ python:3.11-slim bash容器内执行pip install transformers torch --index-url https://download.pytorch.org/whl/cpu python /app/load_model.py如果模型加载过程中弹出了网络请求、异常日志或内存飙升应当立即停止并把模型目录置为不可信。5.5 运行结果与验证要点正常加载成功后终端应该输出类似下面的内容[*] 模型加载成功验证时重点关注三个信号加载日志中是否出现torch.load相关提示。如果出现说明模型可能走了 pickle 路径需要检查原因。加载是否涉及网络访问日志。网络隔离环境下任何对外连接都应该是异常信号。加载后进程是否残留。如果进程退出后还有隐藏子进程基本可以判定模型有问题。如果这一步失败不要急着换模型。先查看完整异常堆栈确认是依赖版本问题、权重缺失问题还是反序列化安全问题。详情见第 7 节。6. 企业级模型供应链治理权限、清单与审计个人开发者的防护做到容器和哈希校验已经不错但企业环境还差得很远。任何一个运维团队都不应该允许开发者不加审批地从公网拉取任意模型到生产服务器。落到工程上至少需要四件事。6.1 建立模型白名单企业应该维护一份内部模型白名单明确哪些模型经过安全审查、可以用于哪些业务、由谁负责更新。开发者需要使用新模型时先走申请流程由安全团队或负责人执行下载、扫描、沙箱验证然后发布到内部模型仓库。这一步看起来繁琐实际上能挡掉大量供应链攻击。白名单制还能让模型版本管理变成可控变更而不是每次都在业务代码里写死一个在线仓库地址。6.2 模型清单与 SBOM把模型当成软件组件管理记录模型的来源、版本、哈希、许可证、依赖框架、上线时间。这在概念上等同于软件物料清单SBOM。一旦某天 Hugging Face 上爆出恶意模型事件我们可以快速排查内部是否有人用过这个模型而不是翻遍所有服务器。推荐使用平台原生的模型管理能力或者用一套简单的内部组件库统一登记。哪怕刚开始只是 Excel 表格也比没有记录强。6.3 最小权限与审计模型推理服务应使用独立系统账号运行不授予 sudo 权限不挂载敏感目录。模型文件目录只读挂载模型进程不允许写入。如果使用容器容器内用户应该是非 root 用户并开启只读根文件系统。同时审计日志必须覆盖模型下载、执行、加载异常、网络连接等关键动作。很多攻击并不是无法发现而是日志缺失导致事后无法溯源。6.4 定期扫描与事件响应企业应该定期对内部模型文件做一次全面扫描包括文件扩展名扫描、哈希比对、依赖审计。如果发现模型文件在未审批的情况下出现在生产环境应该触发告警并记录处置流程。事件响应预案里要专门有一条如果模型加载出现可疑行为第一步是断网隔离然后保留现场日志不要直接删除模型目录因为删掉后取证会变得非常困难。7. 常见问题与排查问题现象可能原因排查方式解决方案模型下载速度很慢网络链路不稳定或跨地域访问检查网络状态确认下载源使用公开镜像站加速但下载后务必做哈希校验加载时报 pickle 相关错误权重格式为.bin走了不安全反序列化查看日志中是否出现torch.load转换权重为 safetensors 后再加载from_pretrained提示需要trust_remote_codeTrue模型包含自定义代码审查模型仓库内.py文件仅在人工确认后开启生产环境默认关闭模型加载成功但推理结果异常可能是模型被打过补丁或投毒对比官方权重哈希从可信渠道重新下载并使用 safetensors容器内无法加载模型容器内缺少依赖库或模型目录未挂载正确检查容器挂载路径和 Python 环境使用docker inspect确认挂载安装依赖后再运行扫描脚本报“损坏的 zip/tar 文件”下载不完整或文件本身被构造重新下载并比对哈希删除本地缓存重新下载指定 commit 版本排查顺序建议固定为先看加载日志再确认文件哈希最后检查依赖版本。不要在没搞清楚异常原因的情况下反复重试否则可能会掩盖恶意代码的触发痕迹。8. 给开发者的最终建议从这次 OpenAl 相关调查事件出发我更想让你记住的是模型是一个可执行环境不是静态文件。任何从公网下载模型的行为都应该默认“不可信”而不是默认“可信”。在最简单的个人项目中至少做到三点固定模型 commit、优先使用 safetensors、关闭trust_remote_code。在稍微正式一点的项目里增加容器隔离和网络限制。在企业环境里模型供应链治理要像治理依赖包一样严肃白名单、清单、审计、应急响应一个都不能少。你不需要变成安全专家但你需要建立起一道习惯性防线。如果这篇文章能让你的模型加载流程里多一次哈希校验、少一次无脑的trust_remote_codeTrue那这次调查事件对你的价值就已经体现出来了。