当前位置: 首页 > news >正文

Python pickle序列化的安全风险与替代方案

Python pickle序列化的安全风险与替代方案

pickle在反序列化时会执行任意代码。这不是bug,而是设计。pickle协议本质上是一个栈虚拟机,控制指令(pickle opcodes)可以创建对象、设置属性、调用函数。

opcode的执行过程:

import pickletools
import pickle

class Exploit:
def __reduce__(self):
import os
return (os.system, ('echo vulnerable',))

payload = pickle.dumps(Exploit())
pickletools.dis(payload)

输出显示pickle字节码的各条指令:

0: \x80 PROTO 5
2: \x95 FRAME 37
11: \x8c SHORT_BINUNICODE 'os'
15: \x93 STACK_GLOBAL
16: \x8c SHORT_BINUNICODE 'system'
24: \x93 STACK_GLOBAL
25: \x8c SHORT_BINUNICODE 'echo vulnerable'
42: \x85 TUPLE1
43: \x87 TUPLE3
44: \x82 BUILD_CLASS
45: . STOP

STACK_GLOBAL指令是关键。它从栈上取出两个字符串(模块名和函数名),通过import和getattr获取任意Python对象。REDUCE指令随后调用这个可调用对象。

解析STACK_GLOBAL的C代码:

static PyObject*
load_global(PicklerObject *self) {
PyObject *name, *module;

name = read_unicode(self); // 读取函数名
module = read_unicode(self); // 读取模块名

// 导入模块
PyObject *module_obj = PyImport_Import(module);
// 获取函数
PyObject *global = PyObject_GetAttr(module_obj, name);

self->stack[++self->stack_size] = global;
}

STACK_GLOBAL不检查从哪个模块导入什么对象。任何模块中的任何可调用对象都可以在反序列化时被调用。

实际的攻击方式比__reduce__更多样。利用R指令(REDUCE)可以直接调用任意函数:

import pickle
import pickletools

# 手动构造pickle字节码,不需要定义任何类
payload = b"\x80\x04\x95\x2e\x00\x00\x00\x00\x00\x00\x00\x8c\x08subprocess\x94\x8c\x03Popen\x93\x94\x8c\x02ls\x85R."

pickletools.dis(payload)

这个payload直接调用了subprocess.Popen('ls'),不需要目标环境中有任何特定的类定义。

防范pickle攻击的方式是限制全局对象的访问。pickle的Unpickler.find_class方法可以重写:

import pickle

class SafeUnpickler(pickle.Unpickler):
ALLOWED_MODULES = {'builtins': {'print', 'range', 'int', 'str', 'list', 'dict'}}

def find_class(self, module, name):
if module in self.ALLOWED_MODULES and name in self.ALLOWED_MODULES[module]:
return super().find_class(module, name)
raise pickle.UnpicklingError(f"禁止访问 {module}.{name}")

data = pickle.dumps([1, 2, 3])
safe = SafeUnpickler(io.BytesIO(data)).load() # OK

bad_data = pickle.dumps(Exploit())
try:
SafeUnpickler(io.BytesIO(bad_data)).load()
except pickle.UnpicklingError:
print("攻击被阻止")

find_class在反序列化时被调用,负责解析模块和名称。重写后只允许白名单内的全局对象。

白名单方式的问题是标准库中有大量"危险但看起来安全"的模块。例如:

# 看起来安全的builtins,但可以做危险操作
import builtins
builtins.exec("import os; os.system('rm -rf /')")

# collections模块也可以
import collections
collections.OrderedDict.__builtins__['exec']("import os; os.system('ls')")

所以find_class的阻止并不能完全保证安全。更安全的做法是完全不使用pickle处理不可信数据。

替代方案:JSON、YAML、MessagePack各有优劣。

JSON的局限性:

import json
data = json.loads('[1, 2, 3]') # OK
# json不支持复杂类型
# json.dumps({1: 2}) # TypeError: keys must be str, int, or float

json_extend支持自定义编码器:

class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
return {'__complex__': True, 'real': obj.real, 'imag': obj.imag}
if isinstance(obj, datetime.datetime):
return {'__datetime__': True, 'value': obj.isoformat()}
return super().default(obj)

def decode_object(dct):
if '__complex__' in dct:
return complex(dct['real'], dct['imag'])
if '__datetime__' in dct:
return datetime.datetime.fromisoformat(dct['value'])
return dct

data = json.dumps(complex(1, 2), cls=ComplexEncoder)
obj = json.loads(data, object_hook=decode_object)
print(obj) # (1+2j)

object_hook是一个安全的解码器,因为它只处理标准JSON类型,不会执行任意代码。

YAML的问题比pickle更严重。PyYAML默认支持任意Python对象:

import yaml
# PyYAML默认load会执行任意代码
yaml.load("!!python/object/apply:os.system ['echo vulnerable']")

yaml.safe_load只处理YAML的标准类型,不处理!!python/object标签:

yaml.safe_load("!!python/object/apply:os.system ['echo vulnerable']")
# yaml.constructor.ConstructorError

这个区别让很多人掉进坑里。看到load明明可用,就用了。应该一直用safe_load,除非有特殊需求。

MessagePack的序列化结果比JSON更紧凑:

import msgpack

data = {'name': 'Alice', 'scores': [90, 85, 95]}
packed = msgpack.packb(data)
unpacked = msgpack.unpackb(packed)
print(len(packed)) # 比json.dumps(data)小约30%

# 自定义类型
import msgpack
from datetime import datetime

def encode_hook(obj):
if isinstance(obj, datetime):
return {'__datetime__': True, 'ts': obj.timestamp()}
return obj

def decode_hook(obj):
if '__datetime__' in obj:
return datetime.fromtimestamp(obj['ts'])
return obj

data = {'now': datetime.now()}
packed = msgpack.packb(data, default=encode_hook)
unpacked = msgpack.unpackb(packed, object_hook=decode_hook)

msgpack本身不执行任意代码,但object_hook中如果执行了危险操作同样不安全。

protobuf的性能比JSON和pickle都好,但需要预定义schema:

syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string tags = 3;
}

Google的protobuf库编译这个.proto文件生成Python代码。序列化时按照schema编码,不涉及任意函数调用,天然安全。

protobuf的使用:

from person_pb2 import Person

p = Person(name="Alice", age=30)
data = p.SerializeToString()
p2 = Person()
p2.ParseFromString(data)
print(p2.name, p2.age)

protobuf的缺点是schema管理成本高,运行时无法动态添加字段。

dill是pickle的扩展版本,可以序列化更复杂的对象:

import dill

# 可以序列化lambda
func = lambda x: x * 2
serialized = dill.dumps(func)
restored = dill.loads(serialized)
print(restored(5)) # 10

# 可以序列化整个会话
dill.dump_session('session.pkl')
# dill.load_session('session.pkl')

dill的安全性比pickle还差,因为它支持更多的Python对象类型。绝对不能用于反序列化不可信数据。

marshal是Python内部的序列化格式,用于.pyc文件:

import marshal

code = compile("print(1+2)", "", "exec")
data = marshal.dumps(code)
# marshal.loads(data)

marshal不支持类实例或自定义对象,但支持code对象。反序列化第三方marshal数据非常危险,因为code对象可以携带任意字节码。

pickle协议版本的发展:

# Python 3.11默认使用协议5
import pickle
print(pickle.DEFAULT_PROTOCOL) # 5

# 协议5支持out-of-band data
buffers = []
data = pickle.dumps(large_array, protocol=5, buffer_callback=buffers.append)
reconstructed = pickle.loads(data, buffers=buffers)

协议5的out-of-band数据允许将大数据缓冲区与pickle流分离,避免在pickle流中复制大块字节数据。对于numpy数组和bytearray等类型,可以大幅提升序列化性能。

为什么Python内部仍然用pickle?因为Python的很多标准库功能依赖pickle的灵活性。multiprocessing用pickle传递数据,functools.lru_cache用pickle做参数哈希,一些数据库驱动用pickle处理复杂类型。完全移除pickle不现实。

正确的做法是区分使用场景:进程间内部通信可以用pickle;任何涉及不可信输入的场景必须用其他序列化方案。

http://www.jsqmd.com/news/1023986/

相关文章:

  • 毕业设计 机器视觉指纹识别特征对比算法(源码+论文)
  • 机器学习理论基石:全面解析GitHub开源项目ML_Notes核心知识点体系与实战应用指南
  • 机器学习工程师书单:按认知断层分级的硬核实战指南
  • 通化闲置黄金变现指南 2026年正规回收门店盘点与防坑技巧 - 润富黄金回收
  • vCenter Server部署运维全解析:从架构选型到证书管理实战
  • 相似性 ≠ 相关性 ≠ 因果性:从蟹化现象到科学推断的方法论陷阱
  • 2026北海黄金回收怎么选商家:实测三家实体门店服务与价格 - 润富黄金回收
  • 2026保姆级教程:证件照换衣服方法,手机/电脑/小程序全套操作指南 - 办公小帮手
  • Simple Keyboard:回归纯粹的Android输入体验
  • 【课程设计/毕业设计】依托 SpringBoot 的竞赛队伍组建及调度系统设计与开发 面向学科竞赛的团队招募与管理系统设计与实现【附源码、数据库、万字文档】
  • Cats Blender插件:VRChat模型优化的5大核心功能与实战指南
  • ML模型生产交付实战:从Notebook到可运维的Real World
  • 2026芜湖黄金回收铂金白银贵金属回收哪家最实在?实地走访 - 鸿运名品
  • 2026年40岁自学C语言还能找到工作吗?是不是有点晚了?
  • 暗黑破坏神2重制版多开解决方案:D2RML令牌管理技术深度解析
  • 2026年北京职务侵占辩护律师怎么选?前部委侦查专家深度解读 - 本地品牌推荐
  • 上海汽车音响门店推荐TOP1:上海冉声汽车音响:20000 台实车验证的品质传奇,定义上海音改行业终极标准,音响改装行业天花板 - 音响改装门店分享
  • 企业级CI/CD构建平台实战:从ctsoft理念到标准化构建服务落地
  • Free NTFS for Mac:打破macOS读写限制的终极免费方案
  • 2026人像抠图保姆级教程!多款人像抠图软件完整操作步骤全解 - 软件小管家
  • AMD Ryzen处理器深度调试指南:5分钟掌握SMU调试工具
  • 环保监测 COD 电极 长效耐用高口碑品牌 - 陈工日常
  • Bagging集成原理与实战:降低模型方差的防抖方案
  • 武汉二手房装修多少钱?2026年最新报价与避坑指南 - 热点速览
  • 2026鞍山黄金回收全攻略 仁瑁福满多万金汇实体门店评测附地址与避坑指南 - 润富黄金回收
  • C# WinForms扫雷实战:GDI+绘制与状态机驱动UI
  • Boss-Key:Windows平台终极隐私保护工具,一键隐藏敏感窗口
  • 终极屏幕实时翻译神器:3分钟解锁跨语言游戏与视频体验
  • Agilent 34401A串口通信避坑指南:为什么你的Python脚本读不到数据?
  • 上海宝格丽首饰回收全指南:7 家平台深度对比,闲置弹簧、小裙子、蛇头这样卖才不吃亏! - 薛定谔的梨花猫