pickle反序列化会执行任意代码,因其本质是执行构造指令流而非解析数据,__reduce__作为默认开启的执行入口,返回(callable, args)后直接调用,且所有协议版本均依赖该机制,无法禁用。

因为 pickle.loads() 和 pickle.load() 不是“解析数据”,而是“执行构造指令”——只要数据来自不可信源,就等于把 Python 解释器的控制权交给了攻击者。
为什么 __reduce__ 是默认开启的执行入口
每个可被 pickle 的对象在反序列化时,只要定义了 __reduce__ 方法,就会被无条件调用。它返回的元组形如 (callable, args),而 pickle 会直接执行 callable(*args)。这不是可选行为,是协议核心逻辑。
- 哪怕你只序列化一个空字典,攻击者也能伪造字节流,让
__reduce__返回(os.system, ("id",)) -
REDUCE操作码无法禁用,所有协议版本(0–5)都依赖它重建对象 - 你无法通过“不定义
__reduce__”来规避——内置类型(如builtins.eval)和第三方类(如subprocess.Popen)本身就能被直接引用并调用
为什么白名单式 Unpickler 常常失效
重写 find_class() 并限制模块/类名,看似可控,实则漏洞百出:
- 攻击者可用
builtins.getattr+ 字符串拼接绕过:比如白名单放行os,但没禁getattr,就能动态调用getattr(os, "system") -
__setstate__、__new__、__init__等钩子函数同样会在还原过程中被触发,且不经过find_class - 白名单只校验“类名是否允许”,不校验“该类实例化后会做什么”——例如
numpy.ndarray安全,但它的__setstate__可被篡改为执行任意代码 - 所有白名单逻辑都假设“字节流未被篡改”,但 pickle 本身不带完整性校验;没加
hmac或签名,白名单形同虚设
为什么 joblib.load / torch.load 也不安全
很多人误以为 joblib.load() 或 torch.load(..., map_location="cpu") 更“模型专用”所以更安全,其实它们底层仍调用 pickle.load() 或 pickle.loads():
-
torch.load()在加载.pkl或.pth时,若文件含恶意 payload,map_location只影响张量设备,不影响反序列化逻辑 -
joblib.load()默认使用 pickle 协议 4,且不启用任何沙箱;它甚至会自动解压并递归反序列化嵌套对象 - 第三方模型平台(如 Hugging Face)上传的
.pkl文件,一旦被部署服务直接load,就等同于运行未知作者的 Python 脚本
json / dataclasses 替代方案为何真正安全
不是因为“它们更简单”,而是设计契约完全不同:它们只处理纯数据,不恢复行为。
-
json.loads()永远不会调用os.system、不会导入模块、不会执行任何函数——它只生成dict、list、str、int等内置类型 - 配合
dataclasses.asdict()或pydantic.BaseModel.model_dump(),你能把对象转成 JSON 友好结构,再用json.dumps()序列化;反向时用MyClass(**data)构造,控制权始终在你自己手里 - 这种分离(数据 vs 行为)才是根本解法:把可序列化的状态和不可序列化的逻辑拆开,而不是试图给 pickle 加锁
最易被忽略的一点:你不需要“完全不用 pickle”,但必须明确区分“可信环境内进程通信”和“跨域接收数据”。前者可以,后者一律禁止——连 redis.get() 返回的值都不能直接传给 pickle.loads(),哪怕它看起来只是个 dict 字节流。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











