pickle.loads()会执行任意代码,因为其本质是执行微型虚拟机指令而非解析数据,通过reduce、global等操作码动态调用函数或导入模块,尤其当对象定义__reduce__方法时,反序列化即触发callable(*args)执行。

为什么 pickle.loads() 会执行任意代码?
因为 pickle.loads() 不是“解析数据”,而是“执行字节码”。pickle 协议本质是一套微型虚拟机指令,REDUCE、GLOBAL 等操作码会在反序列化时动态导入模块、调用函数甚至执行 __reduce__ 返回的元组。攻击者只要构造类似 b"cos\nsystem\n(S'whoami'\ntR." 的字节流,就能触发 os.system()。
这不是 bug,是设计使然——pickle 要还原对象状态,就必须重建执行环境。所以 UnpicklingError 只捕获格式错误,不拦截恶意行为;加 try/except 完全无效。
哪些场景绝对不能用 pickle.load()?
只要数据来源不可信,就该直接放弃 pickle.load():
- 用户上传的
.pkl文件(比如模型、配置) - API 请求体中带的序列化数据(哪怕 Content-Type 是 application/octet-stream)
- 第三方 SDK、开源模型平台(如 Hugging Face、PyTorch Hub)返回的
.pt或.bin文件(它们底层仍可能用 pickle) - 跨服务传输的中间结果(尤其在无鉴权通道中)
注意:torch.save(model, ...) 若保存完整模型(含 model.__dict__),默认走 pickle;而 torch.save(model.state_dict(), ...) 是安全的纯张量字典。
RestrictedUnpickler 怎么写才真正有效?
白名单必须严格到模块+类名两级,且禁止任何副作用路径:
- 只允许
builtins下的dict、list、str等基础类型,不要放行eval、compile - 自定义类必须定义在模块顶层(不能是嵌套类、lambda、
__main__中定义的类),否则find_class()拿不到完整路径 - 禁止放行
os、subprocess、builtins以外的任何模块,哪怕只是numpy.ndarray也需单独验证其安全性 - 示例中
getattr(__import__(module, fromlist=[name]), name)有风险——应提前缓存已知安全类,避免运行时动态 import
即便如此,RestrictedUnpickler 仍无法防御对象初始化时的副作用(比如类的 __init__ 里调用网络请求),它只管“加载哪种类”,不管“加载后干啥”。
替代方案怎么选才不踩坑?
别只看“能不能用”,要看数据结构和上下文:
- 纯结构化数据(字典/列表/数值)→ 用
json:标准库、人可读、天然免疫代码执行 - 张量权重(尤其是 AI 模型)→ 优先
safetensors:Hugging Face 官方推荐,无解释器、无魔法方法、支持签名验证 - 需要跨语言或高性能 →
msgpack或protobuf:需预定义 schema,但杜绝了动态执行风险 - 必须兼容老系统且不能改格式 → 至少加 HMAC 签名验证:
hmac.new(key, data, hashlib.sha256).digest(),反序列化前先校验
最易被忽略的一点:很多团队以为把 .pkl 改成 .json 就安全了,却忘了自定义类的 __dict__ 可能包含不可序列化的对象(如文件句柄、线程锁),导致 json.dumps() 直接崩溃——安全的前提是数据本身可表达。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











