必须禁用pickle.loads()、pickle.load()、marshal.load()、marshal.loads()、yaml.load()(未指定loader)、shelve.open()(非只读模式)等高危反序列化函数,因其处理用户输入可导致rce;应改用json.loads()、msgpack.unpackb()等安全替代方案,并在万不得已时通过白名单unpickler和运行隔离加固。

直接禁用 pickle、marshal、yaml.load() 这类高危反序列化函数,是修复 RCE 的第一道也是最关键的防线。只要它们处理的是用户可控输入,就等于在入口处放了一把带火药的钥匙。
哪些函数必须从用户输入路径彻底移除?
不是“谨慎使用”,而是“禁止出现在任何接收外部数据的代码路径中”。常见误用场景包括:解析 Cookie 中的 session、读取 POST body 的二进制 blob、解码 Base64 编码的参数、反序列化上传的文件内容。
-
pickle.loads()和pickle.load():Python 原生最危险的反序列化入口,支持任意代码执行,无白名单机制 -
marshal.load()和marshal.loads():比 pickle 更底层,不校验类型,极易被绕过检测 -
yaml.load()(未指定Loader):默认使用FullLoader,等价于执行任意 Python 对象构造 -
shelve.open()(未加flag='r'):底层依赖pickle,写模式下可触发反序列化
安全替代方案怎么选?
用什么替代,取决于你原本想存/传什么。别为了“保持兼容”硬套旧逻辑,先厘清数据语义。
- 传结构化数据(如配置、表单)→ 用
json.loads():它只解析基本类型(dict、list、str、int、float、bool、None),不会调用任何魔术方法,天然免疫 RCE - 需要序列化自定义对象 → 改用显式序列化:比如用
dataclasses.asdict()+json,或pydantic.BaseModel.model_dump(),再走 JSON 路径 - 必须用二进制高效存储(如缓存)→ 用
msgpack.unpackb()或orjson.loads():它们不支持代码执行,且比 JSON 更快更紧凑 - 已有历史 pickle 数据要迁移 → 写一次性脚本,用
pickle.load()读一次,转成 JSON 存下来,之后全部切到json.loads()
如果真绕不开 pickle(比如内部 RPC),该怎么加固?
这不是推荐做法,而是“万不得已时的保命操作”。它不能消除风险,只能提高攻击门槛。
- 绝对不要对用户输入调用
pickle.loads();只允许从可信本地文件或内存 buffer(如 Redis 中预置的固定 key)加载 - 启用
pickle.HIGHEST_PROTOCOL并配合自定义Unpickler,重写find_class()方法,只放行白名单里的类名,例如:class SafeUnpickler(pickle.Unpickler): def find_class(self, module, name): if (module, name) not in [('__builtin__', 'dict'), ('builtins', 'list')]: raise ValueError(f"Unsafe class: {module}.{name}") return super().find_class(module, name) - 反序列化前做长度限制和格式校验:比如检查前 4 字节是否为
b'\x80\x04'(Pickle protocol 4 header),防止畸形 payload 触发解析器崩溃 - 运行环境隔离:Web 进程用非 root 用户启动,禁用
os.system、subprocess等模块(通过sys.modules['os'] = None或启动参数-E),让即使触发也无实际执行能力
真正难的不是写几行加固代码,而是把所有隐式依赖反序列化的点都找出来——比如某个中间件自动解包请求体、某个 ORM 的 lazy load 机制、甚至日志系统里对异常对象的 dump。这些地方往往藏得深,修漏一个,RCE 就还在门口等着。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











