萌新一枚,看到大佬的文章感觉好厉害,但是其实看的不是特别明白,就是对于还没学python的我来说,还是有好多细节不理解的,于是写这篇文章记录一下。不一定都是对的,有错误请指正。

首先是个登录框,目录和弱密码爆破无果后,看了下关于我们

然后注册了个用户

看到这以为是ssti,结果试了一下也没有。然后回去看了 下技术细节,发现jsonpickle,去查了下好像有漏洞,


也是改了下权限,但是是个假的flag
于是也看到了这篇文章从源码看JsonPickle反序列化利用与绕WAF-先知社区
我是真菜,比赛的时候以为直接拿它的payload打,然后用下拼接加编码,结果试了半天啥也不是。现在去分析一下为啥不行。先拿下大佬的题目源码分析一下
from flask import Flask, request, make_response, render_template, redirect, url_for
import jsonpickle
import base64
import json
import os
import time
app = Flask(__name__)
app.secret_key = os.urandom(24)
class Account:
def __init__(self, uid, pwd):
self.uid = uid
self.pwd = pwd
class Session:
def __init__(self, meta):
self.meta = meta
users_db = [
Account("admin", os.urandom(16).hex()),
Account("guest", "guest")
]
def register_user(username, password):
for acc in users_db:
if acc.uid == username:
return False
users_db.append(Account(username, password))
return True
FORBIDDEN = [
'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
'__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
'__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]
def waf(serialized):
try:
data = json.loads(serialized)
payload = json.dumps(data, ensure_ascii=False)
for bad in FORBIDDEN:
if bad in payload:
return bad
return None
except:
return "error"
@app.route('/')
def root():
return render_template('index.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
if not username or not password or not confirm_password:
return render_template('register.html', error="所有字段都是必填的。")
if password != confirm_password:
return render_template('register.html', error="密码不匹配。")
if len(username) < 4 or len(password) < 6:
return render_template('register.html', error="用户名至少需要4个字符,密码至少需要6个字符。")
if register_user(username, password):
return render_template('index.html', message="注册成功!请登录。")
else:
return render_template('register.html', error="用户名已存在。")
return render_template('register.html')
@app.post('/auth')
def auth():
u = request.form.get("u")
p = request.form.get("p")
for acc in users_db:
if acc.uid == u and acc.pwd == p:
sess_data = Session({'user': u, 'ts': int(time.time())})
token_raw = jsonpickle.encode(sess_data)
b64_token = base64.b64encode(token_raw.encode()).decode()
resp = make_response("登录成功。")
resp.set_cookie("authz", b64_token)
resp.status_code = 302
resp.headers['Location'] = '/panel'
return resp
return render_template('index.html', error="登录失败。用户名或密码无效。")
@app.route('/panel')
def panel():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root', error="缺少Token。"))
try:
decoded = base64.b64decode(token.encode()).decode()
except:
return render_template('error.html', error="Token格式错误。")
ban = waf(decoded)
if waf(decoded):
return render_template('error.html', error=f"请不要黑客攻击!{ban}")
try:
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta
if meta.get("user") != "admin":
return render_template('user_panel.html', username=meta.get('user'))
return render_template('admin_panel.html')
except Exception as e:
return render_template('error.html', error=f"数据解码失败。")
@app.route('/vault')
def vault():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root'))
try:
decoded = base64.b64decode(token.encode()).decode()
if waf(decoded):
return render_template('error.html', error="请不要尝试黑客攻击!")
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta
if meta.get("user") != "admin":
return render_template('error.html', error="访问被拒绝。只有管理员才能查看此页面。")
flag = "NepCTF{fake_flag_this_is_not_the_real_one}"
return render_template('vault.html', flag=flag)
except:
return redirect(url_for('root'))
@app.route('/about')
def about():
return render_template('about.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=False)
其实flask框架也不是特别懂,这里先看下cookie是怎么验证的吧。

发现登录框的输入框是向/auth路由发post包。
@app.post('/auth')
def auth():
u = request.form.get("u")
p = request.form.get("p")
for acc in users_db:
if acc.uid == u and acc.pwd == p:
sess_data = Session({'user': u, 'ts': int(time.time())})
token_raw = jsonpickle.encode(sess_data)
b64_token = base64.b64encode(token_raw.encode()).decode()
resp = make_response("登录成功。")
resp.set_cookie("authz", b64_token)
resp.status_code = 302
resp.headers['Location'] = '/panel'
return resp
return render_template('index.html', error="登录失败。用户名或密码无效。")
然后看到resp.set_cookie("authz",b64_token),这应该是设置cookie的,然后重定向到/panel
@app.route('/panel')
def panel():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root', error="缺少Token。"))
try:
decoded = base64.b64decode(token.encode()).decode()
except:
return render_template('error.html', error="Token格式错误。")
ban = waf(decoded)
if waf(decoded):
return render_template('error.html', error=f"请不要黑客攻击!{ban}")
try:
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta
if meta.get("user") != "admin":
return render_template('user_panel.html', username=meta.get('user'))
return render_template('admin_panel.html')
except Exception as e:
return render_template('error.html', error=f"数据解码失败。")
这里看到先把token = request.cookies.get("authz"),cookie的authz赋值给token。然后验证cookie是否存在和能否base64解码。然后就进到waf了。
FORBIDDEN = [
'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
'__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
'__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]
def waf(serialized):
try:
data = json.loads(serialized)
payload = json.dumps(data, ensure_ascii=False)
for bad in FORBIDDEN:
if bad in payload:
return bad
return None
except:
return "error"
然后先是
data = json.loads(serialized)
payload = json.dumps(data, ensure_ascii=False)

这里果然不能拼接,查了一下发现
JSON 对键的格式有严格要求:
- 键必须是字符串,且必须用双引号包裹。
这样就没法对标签进行拼接了,然后试下编码

json.loads自动把编码还原了,那这样还没到waf就被还原了,就没法绕过了。
然后如果是正常的数据就会进入
try:
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta
先在看看大佬的绕过方法吧
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},"ts":1753446254}}
说实话不好理解,虽然看大佬的文章,跟着人家的思路知道这个代码可以干嘛,但是具体咋来的我也不懂。现在学习一下吧。
拷打了一下ai
1. 反序列化主入口 (jsonpickle/__init__.py)
# jsonpickle/__init__.py
def decode(string, backend=None, keys=False, safe=False, context=None):
# 创建 Unpickler 实例
unpickler = unpickler.Unpickler(
backend=backend,
keys=keys,
safe=safe,
context=context
)
# ⚠️ 关键接入点:将 JSON 字符串转为 Python 对象
data = backend.decode(string)
# ⚠️ 核心入口:调用 restore 方法
return unpickler.restore(data)
try: sess_obj = jsonpickle.decode(decoded, safe=True)
这里的decode应该就是调用入口。
我们只传了payload和safe
看了下源码
基础反序列化:backend.decode(string)将 JSON 字符串转换为 Python 字典:
确实最后又调用了restore

-
状态重置(
reset参数)
如果reset=True(默认行为),会先调用self.reset(),清空 Unpickler 内部的对象引用缓存、代理对象等状态,确保本次恢复是 “干净” 的。 -
类注册(
classes参数)
如果传入classes(类或类的映射),会调用self.register_classes(classes),将这些类注册到 Unpickler 中。这一步是为了让反序列化时能找到自定义类(比如不在全局作用域的类)。 -
核心恢复逻辑(
self._restore(obj))
这是反序列化的核心:_restore方法会递归遍历obj,识别 jsonpickle 特有的元数据标签(如py/object、py/set、py/newargsex等),并根据标签逻辑重建对象。- 对基础类型(如字符串、字典)直接返回;
- 对包含
py/object的字典,会尝试导入并实例化对应的类; - 对
py/set等集合标签,会重建为 Python 集合对象。
继续进入_restore(obj)

-
类型检查
首先判断obj是否为(str, list, dict, set, tuple)中的一种:python
if not isinstance(obj, (str, list, dict, set, tuple)): restore = _passthrough- 若
obj不属于这些 “容器 / 可迭代” 类型,说明它不可能包含 jsonpickle 的元数据标签(如py/object、py/set等),因此直接用_passthrough函数处理(即直接返回原对象)。 - 注释强调 “不要把类型元组改成集合”,因为
isinstance对集合的判断逻辑与元组不同,会导致错误。
- 若
-
标签解析分支
若obj属于上述类型,调用self._restore_tags(obj):python
else: restore = self._restore_tags(obj)_restore_tags是 jsonpickle 的标签解析核心,会识别obj中是否包含py/object、py/set等元数据标签,并返回对应的处理函数(如实例化类、重建集合等)。
我们的payload之前backend.decode(string)将 JSON 字符串转换为 Python 字典,
是字典进入_restore_tags
_restore_tags里是
def _restore_tags(
self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
) -> Callable[[Any], Any]:
"""Return the restoration function for the specified object"""
try:
if not tags.RESERVED <= set(obj) and type(obj) not in (list, dict):
return _passthrough
except TypeError:
pass
if type(obj) is dict:
if tags.TUPLE in obj:
restore = self._restore_tuple
elif tags.SET in obj:
restore = self._restore_set # type: ignore[assignment]
elif tags.B64 in obj:
restore = self._restore_base64 # type: ignore[assignment]
elif tags.B85 in obj:
restore = self._restore_base85 # type: ignore[assignment]
elif tags.ID in obj:
restore = self._restore_id
elif tags.ITERATOR in obj:
restore = self._restore_iterator # type: ignore[assignment]
elif tags.OBJECT in obj:
restore = self._restore_object
elif tags.TYPE in obj:
restore = self._restore_type
elif tags.REDUCE in obj:
restore = self._restore_reduce
elif tags.FUNCTION in obj:
restore = self._restore_function
elif tags.MODULE in obj:
restore = self._restore_module
elif tags.REPR in obj:
if self.safe:
restore = self._restore_repr_safe
else:
restore = self._restore_repr
else:
restore = self._restore_dict # type: ignore[assignment]
elif type(obj) is list:
restore = self._restore_list # type: ignore[assignment]
else:
restore = _passthrough # type: ignore[assignment]
return restore
方法核心逻辑
该方法接收一个序列化后的对象 obj,返回一个恢复函数(Callable),用于将 obj 转换回原始 Python 对象。
1. 前置检查:过滤无需标签解析的对象
python
try:
if not tags.RESERVED <= set(obj) and type(obj) not in (list, dict):
return _passthrough
except TypeError:
pass
tags.RESERVED是 jsonpickle 的保留标签集合(如py/object、py/set、py/tuple等)。- 检查
obj是否包含保留标签,且类型不是list或dict:- 若满足条件,说明
obj无需标签解析,直接返回_passthrough(即 “直接返回原对象”)。 - 若
obj是不可哈希类型(如list),set(obj)会触发TypeError,此时通过except忽略检查,进入后续分支。
- 若满足条件,说明
2. 字典类型(dict)的标签解析
若 obj 是字典,根据其中包含的保留标签,选择对应的恢复函数:

我们是"py/object"标签进入这个

class_name = obj[tags.OBJECT]
在 Python 中,中括号 [] 通常用于索引操作,表示从某个对象中获取特定位置或键对应的值。在 class_name = obj[tags.OBJECT] 这行代码中,[] 的具体含义取决于 obj 的类型:
1. 字典(dict)索引
如果 obj 是一个字典,obj[key] 表示获取字典中键 key 对应的值。
这里就是把payload的__main__.Session赋值给class_name。
然后通过 loadclass 函数动态导入并获取类对象 cls:
就是导入 __main__.Session 类
其实这个我也没看不太懂源码,。。一步一步来吧。
先看下参数第一个是__main__.Session,第二个是啥self._classes
self是_restore_object的第一个参数
往前看了一下一开始调用 context.restore(data, reset=reset, classes=classes)
的下一步就有self了
def restore(
self, obj: Any, reset: bool = True, classes: Optional[ClassesType] = None
)
ai一下。
在你给出的代码里,context.restore(data, reset=reset, classes=classes) 这行代码中的 context 其实是 Unpickler 类的一个实例。所以,当调用 restore 方法时,self 代表的就是这个 Unpickler 实例。下面来详细分析一下:
- context 的创建过程:
python
运行
context = context or Unpickler(
keys=keys,
backend=backend,
safe=safe,
v1_decode=v1_decode,
on_missing=on_missing,
handle_readonly=handle_readonly,
)
要是用户没有特别提供 context 参数,系统就会自动创建一个 Unpickler 实例,并且把相关的配置参数传递给它。
- 方法调用的本质:
在 Python 里,当你通过实例去调用一个方法时,比如context.restore(...),Python 会把这个实例(也就是context)自动作为第一个参数传递给方法,这个参数就是self。所以,在restore方法的实现中:
def restore(self, obj: Any, reset: bool = True, classes: Optional[ClassesType] = None):
# self 就是调用这个方法的 Unpickler 实例
...
这里的 self 包含了实例的所有属性和配置,像 keys、backend、safe 这些参数都可以通过 self 来访问。
- 方法的作用:
restore方法的主要功能是把 JSON 数据反序列化为 Python 对象。它会依据 JSON 数据的结构,运用Unpickler中配置好的规则和处理器,将数据还原成原始的 Python 对象。
总结来说,self 代表的是 Unpickler 实例,这个实例保存了反序列化过程中需要用到的所有配置信息和状态。
但是好像和self没关。继续ai
1. 处理 "__main__.Session"
步骤解析:
-
参数传入:
module_and_name = "__main__.Session"classes = None(默认未传入自定义类映射)
-
跳过自定义类检查:
python
# loadclass() 源码片段 if classes: # classes 为 None,跳过此分支 try: return classes[module_and_name] except KeyError: # ... -
分割模块名和类名:
python
# loadclass() 源码片段 names = module_and_name.split('.') # ['__main__', 'Session'] -
尝试导入模块并获取类:
- 第一次分割尝试(
up_to = 1):- 模块名:
__main__ - 类名:
Session
python
# loadclass() 源码片段 for up_to in range(len(names) - 1, 0, -1): # up_to = 1 module = util.untranslate_module_name('.'.join(names[:up_to])) # '__main__' try: __import__(module) # 导入 __main__ 模块(当前运行的主模块) obj = sys.modules[module] # 获取 __main__ 模块对象 for class_name in names[up_to:]: # 遍历 ['Session'] obj = getattr(obj, class_name) # 获取 Session 类 return obj # 返回 Session 类对象 except (AttributeError, ImportError, ValueError): continue - 模块名:
- 成功获取:如果当前主模块中定义了
Session类,则返回该类对象。
- 第一次分割尝试(
然后就是返回了session类,
回到原来的函数

-
查找自定义处理器:
python
运行
# _restore_object() 源码片段 handler = handlers.get(cls, handlers.get(class_name)) # 查找 Session 类的自定义处理器handlers是一个全局注册表,存储类与处理器的映射关系。- 如果未找到自定义处理器(通常是这种情况),
handler为None,进入默认逻辑。 - 如果找到自定义处理器,执行
handler(self).restore(obj),但我们先分析默认情况。
-
默认对象恢复逻辑:
- 由于
handler为None,跳过if handler is not None分支,执行后续默认逻辑
- 由于
继续ai
步骤 1:进入 _restore_object_instance 函数,初始化代理
_restore_object_instance 是恢复对象实例的核心函数,首先创建一个代理对象(处理循环引用):
def _restore_object_instance(
self, obj: Dict[str, Any], cls: Type[Any], class_name: str = ''
) -> Any:
# 1. 创建占位代理对象(允许子对象在父对象实例化前引用它)
proxy = _Proxy() # 临时代理,用于处理循环引用
self._mkref(proxy) # 记录代理引用,加入内部引用表
步骤 2:加载工厂函数(_loadfactory)
接下来尝试从 obj 中加载工厂函数(通常用于集合、列表等有默认工厂的类型,此处 Session 类无工厂,返回 None):
# 2. 加载工厂函数(若存在)
factory = self._loadfactory(obj) # 输入 obj 中无工厂相关字段,factory 为 None
步骤 3:解析实例化参数(args 和 kwargs)
从 obj 中提取实例化 Session 类所需的参数(args 和 kwargs):
ai出错了又重开了个会话
前提:明确两个对象的结构
输入数据包含两个带py/object的对象:
- 外层
Session对象:{"py/object": "__main__.Session", "meta": {...}}- 无
py/newargsex标签,只有meta字段。
- 无
- 嵌套的
glob.glob对象:{"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]}, ""]}- 有
py/newargsex标签,包含函数调用参数。
- 有
步骤 1:_restore_object调用_restore_object_instance处理Session对象
Session对象的_restore_object流程结束后,进入_restore_object_instance:
return self._restore_object_instance(obj, cls, class_name) # obj是Session的序列化数据
步骤 2:_restore_object_instance处理Session对象(无py/newargsex)
2.1 初始化代理
def _restore_object_instance(self, obj: Dict[str, Any], cls: Type[Any], class_name: str = '') -> Any:
proxy = _Proxy() # 创建代理
self._mkref(proxy) # 记录代理引用
2.2 加载工厂函数(Session无工厂)
factory = self._loadfactory(obj) # factory = None(Session无工厂相关字段)
2.3 解析Session的实例化参数(无py/newargsex)
# 检查Session的obj是否有py/newargsex标签
if has_tag(obj, tags.NEWARGSEX): # Session的obj中无此标签,条件为False
args, kwargs = obj[tags.NEWARGSEX]
else:
args = getargs(obj, classes=self._classes) # 调用getargs获取默认参数(返回空列表)
kwargs = {} # 无关键字参数
# 恢复参数(因args和kwargs为空,无实际操作)
if args:
args = self._restore(args)
if kwargs:
kwargs = self._restore(kwargs)
- 结果:
args = [],kwargs = {}(Session实例化无需参数)。
2.4 创建Session实例(新式类)
is_oldstyle = not (isinstance(cls, type) or getattr(cls, '__meta__', None)) # 假设为新式类,is_oldstyle = False
try:
if not is_oldstyle and hasattr(cls, '__new__'):
# 调用Session的__new__方法创建实例(无参数)
instance = cls.__new__(cls, *args, **kwargs) # 即 Session.__new__(Session)
else:
instance = object.__new__(cls)
except TypeError:
is_oldstyle = True # 不触发
2.5 替换代理引用
proxy.reset(instance) # 代理指向真实实例
self._swapref(proxy, instance) # 引用表中替换为真实实例
2.6 恢复Session的实例变量(处理meta字段)
instance = self._restore_object_instance_variables(obj, instance) # 关键:处理meta字段
步骤 3:_restore_object_instance_variables恢复Session的meta属性
完啦,大家对不起,之前的版本到这后面都是ai自动补的源码了,我服了,这ai也不提示,我源码是github下,大家还是对着源码看比较不容易错。
步骤 1:进入 _restore_object_instance_variables 函数
该函数是恢复实例属性的入口,核心逻辑分三步:
def _restore_object_instance_variables(
self, obj: Dict[str, Any], instance: Any
) -> Any:
# 1. 从字典恢复属性(处理 obj 中的字段,如 meta)
instance = self._restore_from_dict(obj, instance)
# 2. 处理序列子类(如 list/set 子类,当前 Session 实例不涉及)
if has_tag(obj, tags.SEQ): # obj 中无 "py/seq" 标签,条件不成立
if hasattr(instance, 'append'):
...
elif hasattr(instance, 'add'):
...
# 3. 处理状态数据(当前 Session 实例不涉及)
if has_tag(obj, tags.STATE): # obj 中无 "py/state" 标签,条件不成立
instance = self._restore_state(obj, instance)
return instance # 返回恢复属性后的实例
- 核心流程集中在第一步:
_restore_from_dict(obj, instance)。
步骤 2:_restore_from_dict 处理 Session 的字段(重点是 meta)
_restore_from_dict 是恢复属性的核心函数,作用是遍历 obj 中的键值对(排除保留标签),递归恢复值并赋值给 Session 实例。
2.1 初始化变量
def _restore_from_dict(
self,
obj: Dict[str, Any],
instance: Any,
ignorereserved: bool = True,
restore_dict_items: bool = True,
) -> Any:
restore_key = self._restore_key_fn() # 获取键恢复函数(此处用于处理字典键)
method = _obj_setattr # 属性设置方法(默认使用 setattr)
deferred = {} # 存储延迟设置的属性(用于不可变对象)
2.2 遍历 obj 中的键值对
obj 中包含 {"py/object": "__main__.Session", "meta": {...}},遍历所有键:
for k, v in util.items(obj): # k 依次为 "py/object"、"meta"
# 跳过保留标签(如 "py/object")
if ignorereserved and k in tags.RESERVED: # "py/object" 是保留标签,跳过
continue
# 处理键名(转换为字符串,用于命名栈)
if isinstance(k, (int, float)):
str_k = k.__str__()
else:
str_k = k # k 为 "meta" 时,str_k = "meta"
self._namestack.append(str_k) # 将 "meta" 加入命名栈(用于调试/跟踪)
这里util.items()我去看了下源码
具体来说,这个函数做了这些事情:
-
接收两个参数:
obj:一个映射类型(Mapping[K, V])的对象,比如字典exclude:一个可迭代对象,包含需要排除的键(默认是空元组,即不排除任何键)
-
函数逻辑:
- 遍历
obj中的所有键值对(k, v) - 如果当前键
k在exclude列表中,则跳过(continue) - 否则,通过
yield返回这个键值对(k, v)
- 遍历
-
注释说明:
这个函数不能简单地用dict.items()替代,因为它有exclude参数可以排除特定键,目前需要保留这个函数。
其实只传了一个参数obj。没用这函数的排除功能

后面自己写了。
又去拷打ai了。。。。。。。
_restore_from_dict 处理 meta 键
_restore_from_dict 方法负责遍历 obj 中的所有键(排除保留标签如 py/object),并将值恢复后赋值给实例。对于 meta 键:
def _restore_from_dict(
self,
obj: Dict[str, Any],
instance: Any,
ignorereserved: bool = True,
restore_dict_items: bool = True,
) -> Any:
restore_key = self._restore_key_fn() # 用于恢复键(当前案例中为普通字符串键)
method = _obj_setattr # 用于设置属性的方法(默认用setattr)
deferred = {}
for k, v in util.items(obj):
# 忽略保留标签(如py/object),只处理用户自定义键(如meta)
if ignorereserved and k in tags.RESERVED:
continue
# 记录当前键名到命名栈(用于引用追踪)
str_k = str(k)
self._namestack.append(str_k)
# 恢复键和值(当前案例中键为"meta",值为嵌套字典)
if restore_dict_items:
k = restore_key(k) # 键"meta"无需特殊处理,直接保留
value = self._restore(v) # 递归恢复值(即meta对应的嵌套字典)
# 将恢复后的值赋值给Session实例的meta属性
if not k.startswith('__'):
try:
setattr(instance, k, value) # 等价于 session.meta = 恢复后的meta字典
except AttributeError:
# 处理只读属性等异常(当前案例不涉及)
...
# 如果值是代理对象,记录下来后续替换(当前案例中暂时不涉及)
if isinstance(value, _Proxy):
self._proxies.append((instance, k, value, method))
self._namestack.pop() # 退出当前键的命名栈
return instance
关键步骤:
- 遍历到
k="meta"时,v是{"user": {...}, "ts": 1753446254}。 - 调用
self._restore(v)递归处理meta的值(嵌套字典),进入下一步。
3. 递归处理 meta 的值(嵌套字典)
self._restore(v) 会对 meta 的值(嵌套字典)进行反序列化。根据 _restore 方法的逻辑:
def _restore(
self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
) -> Any:
if not isinstance(obj, (str, list, dict, set, tuple)):
return _passthrough(obj)
else:
restore = self._restore_tags(obj) # 确定恢复方法
return restore(obj)
由于 v 是字典,_restore_tags 会返回 _restore_dict 作为处理方法(因为字典中没有特殊标签如 py/object,仅为普通字典)。
4. _restore_dict 处理 meta 的嵌套字典
_restore_dict 方法用于恢复普通字典(非对象),遍历其键值对(user 和 ts):
def _restore_dict(self, obj: Dict[str, Any]) -> Dict[str, Any]:
data = {}
if not self.v1_decode:
self._mkref(data) # 记录引用(用于循环引用处理)
# 遍历字典中的键值对(user和ts)
for k, v in util.items(obj):
str_k = str(k)
self._namestack.append(str_k) # 记录键名("user"或"ts")
# 恢复值(递归处理user和ts)
data[k] = result = self._restore(v)
# 如果值是代理对象,记录后续替换(当前案例中暂时不涉及)
if isinstance(result, _Proxy):
self._proxies.append((data, k, result, _obj_setvalue))
self._namestack.pop()
return data
此时 data 会被填充为 {"user": 恢复后的user对象, "ts": 1753446254},并作为 meta 的值返回给上一层,最终通过 setattr 赋值给 Session 实例的 meta 属性。
5. 处理 user 键(glob.glob 函数)
user 对应的值是 {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},属于带 py/object 标签的字典,处理流程如下:
-
_restore触发_restore_tags:由于字典包含py/object,_restore_tags返回_restore_object方法。 -
_restore_object加载函数:def _restore_object(self, obj: Dict[str, Any]) -> Any: class_name = obj[tags.OBJECT] # "glob.glob" cls = loadclass(class_name, classes=self._classes) # 加载glob.glob函数 # 无自定义处理器,进入实例恢复 return self._restore_object_instance(obj, cls, class_name)loadclass("glob.glob")会导入glob模块并返回glob.glob函数(函数也是对象)。
-
_restore_object_instance处理参数:def _restore_object_instance(self, obj: Dict[str, Any], cls: Type[Any], class_name: str = '') -> Any: # 处理py/newargsex参数 if has_tag(obj, tags.NEWARGSEX): args, kwargs = obj[tags.NEWARGSEX] # args = [{"py/set":["/*"]}, ""], kwargs = {} args = self._restore(args) # 递归恢复参数:将{"py/set":["/*"]}恢复为set(["/*"]) kwargs = self._restore(kwargs) # 空字典 # 函数无需实例化,直接返回(此处cls是glob.glob函数) instance = cls # 函数本身作为"实例" # 恢复其他变量(当前案例无额外变量) instance = self._restore_object_instance_variables(obj, instance) return instance- 最终
user键的值被恢复为glob.glob函数,且参数已处理为set(["/*"])(后续调用函数时会使用这些参数)。
- 最终
6. 处理 ts 键(整数)
ts 对应的值是 1753446254(整数),处理流程如下:
_restore方法判断其为非容器类型,直接返回原值(_passthrough)。- 在
_restore_dict中,data["ts"]被赋值为1753446254。
最终结果
Session 实例的 meta 属性被设置为:
{
"user": glob.glob, # 函数对象,参数已处理为set(["/*"])
"ts": 1753446254 # 整数
}
总结关键源码调用链
plaintext
_restore_object_instance_variables
└── _restore_from_dict(处理meta键)
└── _restore(meta的值:嵌套字典)
└── _restore_tags → _restore_dict(处理user和ts)
├── _restore(user的值:带py/object的字典)
│ ├── _restore_tags → _restore_object
│ │ ├── loadclass(加载glob.glob)
│ │ └── _restore_object_instance(处理参数)
│ └── 返回glob.glob函数
└── _restore(ts的值:整数)
└── _passthrough(直接返回1753446254)
好难。。。

1437

被折叠的 条评论
为什么被折叠?



