aiohttp 修复 `CookieJar.update_cookies()` 未复制用户传入的可变 `Morsel` 对象的缺陷解析

发布时间:2026/9/21 22:44:18
aiohttp 修复 `CookieJar.update_cookies()` 未复制用户传入的可变 `Morsel` 对象的缺陷解析 后端Web框架WebSocket【免费下载链接】aiohttpAsynchronous HTTP client/server framework for asyncio and Python项目地址https://gitcode.com/gh_mirrors/ai/aiohttp点击查看免费下载本篇文章围绕 aiohttp 变更日志条目 CHANGES/13637.bugfix.rstFixedCookieJar.update_cookies()to copy user-passed mutableMorselobjects展开深入剖析该 bugfix 的缺陷成因、修复方式与底层实现原理。通过阅读本文你将理解 aiohttp 客户端 cookie 存储模型中的对象所有权与防御性复制机制掌握CookieJar.update_cookies()、update_cookies_from_headers()与Morsel可变对象之间的交互细节并学会如何在业务代码中规避同类共享可变对象被意外修改的经典问题。变更日志说了什么CHANGES/13637.bugfix.rst全文仅一句话却概括了一次典型的行为修复FixedCookieJar.update_cookies()to copy user-passed mutableMorselobjects -- by :user:Dreamsorcerer.拆解其含义修复对象aiohttp.CookieJar的公开方法update_cookies()修复内容该方法现在会复制调用方传入的可变Morsel对象而非直接持有引用修复目的保证调用方在调用之后继续修改自己手中的Morsel时不会意外改写 cookie jar 内部已存储的 cookie 状态同时jar 内部的规范化处理如补全domain、path也不会反向污染调用方的对象。要真正理解这条修复需要先弄清 aiohttp 中 cookie 存储的核心数据结构。背景CookieJar与Morsel的对象模型CookieJar 是客户端 cookie 的存储与过滤中心在 aiohttp 中CookieJar定义于 aiohttp/cookiejar.py实现了遵循 RFC 6265 的 cookie 存储构造参数unsafe是否接受来自 IP 地址主机的 cookie、quote_cookie发送时是否对 cookie 值做编码与treat_as_secure_origin将某些来源视为安全来源内部使用defaultdict[tuple[str, str], SimpleCookie]按(domain, path)分组存放 cookiecookie 的完整身份是(domain, path, name)三元组host_only_cookies集合记录未携带Domain属性的主机限定 cookiefilter_cookies()负责按请求 URL 的域名、路径前缀、secure 标记等规则筛选出应当发送的 cookie。CookieJar在整个客户端生命周期中的位置可从 aiohttp/client.py 看出ClientSession.__init__在cookie_jar参数缺省时默认创建CookieJar()client.py#L379-L381并在构造时把用户传入的cookies参数一次性写入 jarif cookies: self._cookie_jar.update_cookies(cookies)也就是说update_cookies()是用户向会话播种 cookie 的主要入口它的健壮性直接影响整个会话的 cookie 行为。Morsel 是 Python 标准库中的可变 cookie 单元Morsel来自 Python 标准库http.cookies用于描述单个 cookie 的key、value、coded_value及domain、path、secure等属性。它本质上是可变对象——可以通过morsel.set()或属性赋值来修改其值。aiohttp 的LooseCookies类型见 aiohttp/typedefs.py允许update_cookies()接受多种形态的输入LooseCookiesMappings Mapping[str, str | BaseCookie[str] | Morsel[Any]] LooseCookiesIterables Iterable[tuple[str, str | BaseCookie[str] | Morsel[Any]]] LooseCookies LooseCookiesMappings | LooseCookiesIterables | BaseCookie[str]即字符串、BaseCookie如SimpleCookie或单个Morsel对象都可以作为 cookie 来源。问题恰恰出在调用方直接传入Morsel这条路径上。缺陷本质共享可变对象导致的相互污染传入 Morsel 时的两种污染方向在没有防御性复制的情况下如果 jar 直接保存调用方传入的Morsel引用会出现两个方向的互相污染调用方改写 jar调用方在update_cookies()之后继续修改自己手中的Morsel例如登录流程中复用同一个SimpleCookie对象、刷新 token 后原地改值jar 中已存储的 cookie 会被同步篡改后续filter_cookies()发出的Cookie头随之出错jar 改写调用方update_cookies()内部会对 cookie 做规范化处理——例如根据response_url补全缺失的path取响应路径的目录部分、无Domain时把domain设为请求主机、去掉域名首部的点号、去除 path 末尾的斜杠等。如果直接修改传入对象这些规范化会泄漏回调用方导致调用方手中的Morsel属性被意外改写。修复前的实际表现仓库测试 tests/test_cookiejar.py 中新增的两个回归测试均引用 issue #13634与本次变更日志 13637 同源精确刻画了上述两种污染def test_update_cookies_copies_caller_morsel() - None: Test that mutating a Morsel after update_cookies() does not change the jar. jar CookieJar() url URL(http://example.com/) sc SimpleCookie() sc[auth] original-value jar.update_cookies({auth: sc[auth]}, url) # Mutate the callers Morsel after the jar has stored it. sc[auth].set(auth, mutated-value, mutated-value) assert jar.filter_cookies(url)[auth].value original-value def test_update_cookies_does_not_mutate_caller_morsel() - None: Test that update_cookies() normalization does not leak into the callers Morsel. jar CookieJar() sc SimpleCookie() sc[sid] value jar.update_cookies({sid: sc[sid]}, URL(http://example.com/sub/page)) # The jar normalizes its private copy, not the callers object. assert sc[sid][domain] assert sc[sid][path] filtered jar.filter_cookies(URL(http://example.com/sub/page)) assert filtered[sid].value value第一个测试断言事后篡改调用方 Morsel 不影响 jar正向隔离第二个测试断言jar 的 domain/path 规范化不反向写回调用方对象反向隔离。修复实现copy_morsels参数与两阶段处理修复后的调用关系修复的核心落在 aiohttp/cookiejar.py 的update_cookies()与内部方法_update_cookies()上def update_cookies(self, cookies: LooseCookies, response_url: URL URL()) - None: Update cookies. self._update_cookies(cookies, response_url, copy_morselsTrue) def update_cookies_from_headers( self, headers: Sequence[str], response_url: URL ) - None: Update cookies from raw Set-Cookie headers. if headers and (cookies_to_update : parse_set_cookie_headers(headers)): # The freshly parsed Morsels are not shared with the caller, # so they can be stored and normalized without a defensive copy. self._update_cookies(cookies_to_update, response_url, copy_morselsFalse)两个公开入口共享同一个_update_cookies()内部实现通过布尔参数copy_morsels区分是否需要防御性复制update_cookies()面向用户传入的、可能与调用方共享的对象copy_morselsTrueupdate_cookies_from_headers()面向从Set-Cookie原始头解析出来的、新创建且不与任何外部代码共享的Morsel由 aiohttp/_cookie_helpers.py 的parse_set_cookie_headers()生成因此copy_morselsFalse省去一次不必要的复制开销。复制发生的精确位置在_update_cookies()的处理循环中for name, cookie in cookies: if not isinstance(cookie, Morsel): tmp SimpleCookie() tmp[name] cookie # type: ignore[assignment] cookie tmp[name] elif copy_morsels: # TODO(https://github.com/python/typeshed/pull/16346): Remove cast cookie cast(Morsel[str], cookie.copy())处理逻辑分三种情况传入的是普通str非Morsel先装入临时SimpleCookie得到标准Morsel该对象本就是新建的天然与调用方无共享无需复制传入的是Morsel且copy_morselsTrue即经由update_cookies()调用Morsel.copy()得到独立副本此后所有 domain/path 规范化与属性写入都作用于副本传入的是Morsel且copy_morselsFalse即经由update_cookies_from_headers()直接复用解析产物。Morsel.copy()是 Python 标准库http.cookies.Morsel提供的方法返回一个深拷贝含key、value、coded_value及全部保留属性足以切断与源对象的别名关系。复制之后的规范化流程获得私有副本后_update_cookies()继续执行 RFC 6265 相关的规范化这些步骤现在只影响 jar 内部副本拒绝来自 IP 地址主机的 cookie除非unsafeTrue去掉domain末尾的点号、剥离首部的点号如.example.com→example.com校验domain与response_url主机是否匹配_is_domain_matchexample.com可匹配www.example.com子域无path时按响应 URL 的路径取目录前缀如/sub/page→/subpath统一去除末尾斜杠无domain且存在主机名时将 cookie 记入host_only_cookies并把domain设为该主机反之显式Domain则从host_only_cookies中移除对应身份因为域 cookie 会取代同身份的 host-only cookie解析max-age/expires并调度过期_expire_cookie维护小顶堆_expire_heap配合_do_expiration()惰性清理过期条目写入self._cookies[(domain, path)][name]并清空对应的_morsel_cache缓存项保证下次filter_cookies()重建发送用 Morsel。所有这些就地修改都只发生在复制出的对象上——这正是两个回归测试能够同时通过的原因。从调用链看修复的实际价值ClientSession 初始化播种 cookiesClientSession(cookies...)是update_cookies()最常见的使用场景aiohttp/client.py#L383-L384。修复前如果用户复用同一个SimpleCookie对象去创建多个会话或在同一会话内重复使用jar 之间或 jar 与用户对象之间会互相干扰修复后每个会话都持有独立的 cookie 副本行为可预测。单次请求的临时 cookie jar在处理单次请求级cookies参数时aiohttp/client.py#L649-L657 会构造一个临时CookieJar并把请求级 cookies 写入其中再与 jar 结果合并if cookies is not None: tmp_cookie_jar CookieJar( unsafeself._cookie_jar.unsafe, quote_cookieself._cookie_jar.quote_cookie, ) tmp_cookie_jar.update_cookies(cookies) req_cookies tmp_cookie_jar.filter_cookies(url) if req_cookies: all_cookies.load(req_cookies)这里同样依赖update_cookies()的防御性复制传入的cookies可能包含用户直接构造的Morsel若不加复制临时 jar 的规范化会反向改写用户对象且后续请求中用户复用该对象时又会把陈旧值带进新的临时 jar。与响应侧解析路径的对照update_cookies_from_headers()是响应侧路径——ClientSession._request在收到Set-Cookie响应头后调用它aiohttp/client.py#L758。这条路径传入的是 aiohttp/_cookie_helpers.py 中parse_set_cookie_headers()新解析出的Morsel列表对象完全由 aiohttp 内部创建、不对外共享因此可以安全地跳过复制以节省性能。同一修复同时保留了两条路径各自的最优策略是按对象所有权决定复制策略的典型设计。相关演进脉络update_cookies()并非孤立修复CookieJar在近期版本中经历了多轮与对象所有权相关的加固彼此印证3.14.1CookieJar.save()/load()重构——load()改为先清空再写入而非合并且加载的 cookie 走与update_cookies()相同的接受规则并持久化绝对过期时间戳与 host-only 标记见 CHANGES.rst 3.14.1 版本段issue 128243.14.0修复unsafeTrue时请求级 cookies 发往 IP 地址主机被丢弃的问题issue 12011其临时 jar 的unsafe设置需要从会话 jar 复制3.14.0修复 per-requestcookies在跨域重定向时被错误丢弃的问题issue 12550。这些修复共同强化了 aiohttp 客户端 cookie 行为的确定性与隔离性而 13637 这条则是最基础的一环确保 jar 与调用方之间不发生可变对象别名共享。对使用者的启示无需改变调用习惯修复对 API 完全透明update_cookies(mapping)、update_cookies(SimpleCookie)、update_cookies(iterable_of_Morsel)的用法都不变可以放心复用 Morsel 对象修复之后你可以安全地持有并继续修改自己构造的Morseljar 内部的存储不会再受其影响也不会反过来被 jar 修改调试线索如果遇到明明 set 了 cookies 却发出旧值或调用方 Morsel 的 domain/path 莫名被填上的诡异行为且使用的 aiohttp 版本较老应优先考虑升级到包含 13637 修复的版本可验证的回归测试tests/test_cookiejar.py中的test_update_cookies_copies_caller_morsel与test_update_cookies_does_not_mutate_caller_morsel是本次修复的行为契约任何未来改动都必须保持这两个隔离性断言成立。总结CHANGES/13637.bugfix.rst记录了一次小而关键的防御性编程修复CookieJar.update_cookies()现在会对用户传入的可变Morsel执行copy()从根源上切断调用方与 cookie jar 之间的共享可变状态。其实现通过_update_cookies()的copy_morsels开关对用户提供的对象与内部解析的对象采取差异化的复制策略既保证了正确性又没有给响应头解析这一高频路径增加额外开销。理解这条修复也就理解了 aiohttp 客户端 cookie 存储中对象所有权决定复制策略的核心设计思想。赞分享后端Web框架WebSocket【免费下载链接】aiohttpAsynchronous HTTP client/server framework for asyncio and Python项目地址https://gitcode.com/gh_mirrors/ai/aiohttp点击查看免费下载相关推荐ClickHouse v23.8.15.35-lts 变更解读9 项用户可见缺陷修复与发布机制改进ClickHouse v23.8.15.35 lts 变更解读9 项用户可见缺陷修复与发布机制改进 本文以 ClickHouse 官方发布说明 docs/ch数据库OLAP列式数据库大数据实时分析数据分析ClickHouse v25.6.8.10-stable 版本变更深度解读S3 备份重试风暴抑制与对象存储缺陷修复ClickHouse v25.6.8.10 stable 版本变更深度解读S3 备份重试风暴抑制与对象存储缺陷修复 本篇基于 ClickHouse 官方 ch数据库OLAP列式数据库大数据实时分析数据分析Leaflet 1.3.3 缺陷修复发布解析全局变量 L 暴露问题的成因、修复与构建机制Leaflet 1.3.3 缺陷修复发布解析全局变量 L 暴露问题的成因、修复与构建机制 Leaflet 1.3.3 是 2018 年 7 月 18 日发布的前端数据可视化GIS上一篇3Blue1Brown 数学动画开源源码用 Manim 复现那些高播放量的数学视频下一篇Windows 11 开始菜单磁贴被系统更新清空ExplorerPatcher 快速恢复完整布局指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考