pickle.load() 不能读不可信数据,因为它反序列化时会执行任意代码而非仅解析数据,恶意构造的 __reduce__ 或 __setstate__ 可调用 os.system 等危险操作。

为什么 pickle.load() 不能直接读不可信数据
因为 pickle 反序列化会执行任意代码,不是“解析数据”,而是“重建对象+触发方法”。只要输入里藏了恶意构造的 __reduce__ 或 __setstate__,就能调用 os.system、写文件、连外网。
常见错误现象:AttributeError: 'module' object has no attribute 'xxx' 看似是模块缺失,其实是反序列化时尝试导入不存在的恶意模块;更隐蔽的是进程静默拉起、磁盘突然多出临时文件。
使用场景中,最容易踩坑的是:用 pickle 做网络传输载荷(如 Celery 旧配置)、本地缓存未校验、Web 表单提交二进制字段后直接 pickle.load(request.body)。
替代方案选哪个:json、msgpack 还是 dataclass + typing
json 最安全,但只支持基础类型(dict、list、str、int、float、bool、None),无法还原自定义类或函数。
立即学习“Python免费学习笔记(深入)”;
msgpack 比 json 更紧凑、更快,但默认仍不支持自定义类;开启 strict_map_key=False 或用 ext_type 手动注册解码器后,才可能带类型信息——此时必须严格校验 ext_type.code 范围,否则又绕回反序列化执行风险。
如果必须保留类结构,推荐 dataclass + asdict() / from_dict()(配合 dacite 或手写校验):
from dataclasses import dataclass
from dacite import from_dict
<p>@dataclass
class User:
name: str
age: int</p><h1>安全:只从 dict 构建,不执行任意代码</h1><p>user = from_dict(data_class=User, data={"name": "alice", "age": 30})关键点:所有字段类型在运行时静态可检,无魔法方法调用,无隐式 import。
旧系统没法换格式?至少加三道过滤
若必须兼容存量 pickle 流,不能只靠“信任内网”或“加个签名”——签名只防篡改,不防合法 payload 里的恶意逻辑。
必须做:
- 用
RestrictedUnpickler子类重写find_class(),白名单控制可导入模块和类名,例如只允许__builtin__.dict、datetime.datetime - 在反序列化前,用
ast.literal_eval()尝试解析原始字节为字面量(仅适用于简单结构),失败则拒绝 - 启动独立沙箱进程(如
subprocess.run(..., timeout=1))做反序列化,超时或非零退出立即丢弃结果
注意:find_class 白名单要细到类级别,比如允许 collections.OrderedDict 但禁止 subprocess.Popen——后者常被漏掉,因为看起来不像“危险类”。
配置文件里写 eval() 或 exec() 同样危险
有人把 pickle 换成 eval(repr(obj)),以为“没用 pickle 就安全”,其实一样执行任意代码。比如 eval("__import__('os').system('id')")。
真实案例:Django 的 SECRET_KEY 配置误写成 eval(os.environ.get('KEY')),攻击者通过环境变量注入恶意字符串。
正确做法:
- 配置值统一走
os.environ.get('KEY', 'default')+ 类型转换(int()、bool()) - 复杂结构用
json.loads(os.environ.get('CONFIG_JSON', '{}')),再手动映射到对象 - 绝对不要在生产环境启用
debug=True且暴露django.views.debug,它内部用了pprint和repr,可能触发对象的__repr__方法执行副作用
最易被忽略的是日志打印:logger.info("user=%s", user_obj) 如果 user_obj.__repr__ 里有数据库查询或 HTTP 请求,就变成隐式远程调用。










