0xmrma/CVE-2026-34828
GitHub: 0xmrma/CVE-2026-34828
该项目是对 listmonk 邮件列表管理器中会话持久化漏洞(CVE-2026-34828)的完整安全研究报告,详细记录了密码重置与密码修改后旧会话未被撤销的信任边界失效问题及其复现过程。
Stars: 1 | Forks: 0
# CVE-2026-34828
listmonk 在密码重置和密码修改后的会话持久化问题
## 简介
我在审查 **listmonk**(一个开源的时事通讯和邮件列表管理器)时发现了这个问题,当时我脑海中有一个简单的安全问题:
**当用户更改或重置密码时,应用程序真的会终止之前已颁发的会话吗?**
在这种情况下,答案是否定的。
之前颁发的已认证会话在以下两种操作后均保持有效:
- **密码重置**
- **密码修改**
这意味着被盗的会话 cookie 能够在用户赖以恢复账户的关键安全事件中存活下来。
该问题被接受并分配了编号 **CVE-2026-34828**。
**项目:** [GitHub 上的 listmonk](https://github.com/knadh/listmonk)
**CVE:** CVE-2026-34828
这影响了 listmonk,这是一个拥有超过 **500 万次** Docker 拉取量的广泛使用项目。
## 攻击链
`被盗的已认证会话 → 受害者重置或修改密码 → 旧会话仍然有效 → 攻击者在凭证恢复后仍保留账户访问权限`
## listmonk 的功能
**listmonk** 是一个自托管的邮件列表和时事通讯管理器。
它提供:
- 管理员身份验证
- 用户管理
- 活动创建
- 订阅者管理
- SMTP 和操作设置
- 基于浏览器的后台管理
这意味着它的会话模型是一个真正的安全边界。
这里的重要问题不在于 listmonk 是否支持密码重置。
真正的问题是:
**如果会话已经被盗,密码重置或密码修改真的能撤销攻击者的持久访问权限吗?**
在这种情况下,并不能。
## 为什么这个漏洞值得关注
许多安全审查过于狭隘地关注登录绕过和明显的权限提升。
这遗漏了一类重要的弱点:
**恢复失败**
如果用户更改或重置密码,该操作本应具有实际意义。
它本应降低对旧凭证和旧身份验证状态的信任度。
如果攻击者已经拥有一个有效的会话,并且该会话在恢复事件后依然存活,那么受害者实际上并没有完全恢复其账户。
这就是这里的问题所在。
这不是一个登录验证 bug。
不是加密问题。
也不是密码哈希失败。
这是一个 **会话生命周期失败**:
- 密码状态改变了,
- 账户恢复发生了,
- 但旧的会话仍然受信任。
这足以造成一个真正的漏洞。
## 我关注的边界
我并没有通过随机测试接口并指望某个接口崩溃的方式来接触 listmonk。
更有效的方法是首先识别出最高价值的信任边界。
对于重度依赖身份验证的软件,最好测试的边界之一是:
这个问题通常在以下场景中变得有趣:
- 密码重置
- 密码修改
- 2FA 变更
- 账户恢复流程
在 listmonk 中,最强烈的信号来自前两个场景。
问题正是在这里变得清晰可见的。
## 根本原因
这个 bug 并不是密码修改失败了。
这个 bug 在于 **会话比密码的有效期更长**。
从源码审查来看,密码重置流程:
- 生成并验证了一次性重置 token,
- 更新了密码,
- 创建了新会话,
但没有任何可见的旧会话撤销操作。
同样的模式也出现在经过身份验证的密码修改流程中:
- 密码被更新了,
- 但较旧的、已颁发的会话并没有被失效处理。
这种行为与实际测试的结果完全一致。
我审查的相关代码区域包括:
- `cmd/auth.go`:处理忘记/重置密码行为
- `cmd/users.go`:处理已认证用户的个人资料更新
- `internal/core/users.go`:处理密码更新逻辑
### 为什么这是可利用的
因为会话盗窃是一种真实的攻击条件。
一旦攻击者通过任何方式获取了有效的已认证会话 cookie,例如:
- 浏览器被入侵
- 恶意软件
- 共享工作站的访问权限
- 其他组件中的 XSS
- 代理或调试数据泄漏
- 意外的 cookie 暴露
受害者应该能够通过修改或重置密码来终止攻击者的持久访问。
然而在这里,他们做不到。
攻击链非常直接:
- 攻击者拥有一个有效的会话 cookie
- 受害者执行密码重置或密码修改
- 旧密码失效
- 新密码生效
- 攻击者的旧会话仍然能够成功通过身份验证
这就是整个漏洞的本质。
## 为什么这是一个安全问题,而不仅仅是应用行为
重要的区别在于恢复后的持久性。
许多应用程序将密码更改视为纯粹的凭证级别事件。
这是不够的。
真正的问题不是:
真正的问题是:
在 listmonk 中,它没有做到。
这使得原本普通的账户维护变成了不完整的安全恢复。
这就是以下两者之间的区别:
- 普通的会话连续性
- 和真正的安全弱点
## PoC
我在两个独立的流程中验证了这个问题。
### 案例 1:密码重置未撤销现有会话
首先,我创建了一个普通测试用户并登录,保存了已认证的会话 cookie。
然后我触发了忘记密码流程,捕获了重置链接,并重置了密码。
重置后:
- **旧密码不再有效**
- **新密码生效**
- 但**重置前的旧会话 cookie 仍然能够成功通过身份验证**
一个代表性的验证请求如下所示:
```
GET /api/profile HTTP/1.1
Host: 127.0.0.1:9000
Cookie: session=
```
服务器仍然返回了:
```
HTTP/1.1 200 OK
Content-Type: application/json
```
以及已认证的用户资料。
这确立了核心观点:
- 恢复已完成,
- 凭证已更改,
- 但现有会话的信任度依然未受影响。
### 案例 2:密码修改未撤销并行的活跃会话
然后我在经过身份验证的密码修改流程中验证了同一类 bug。
我以同一用户登录了两次,并保存了两个有效的已认证会话:
- 会话 A
- 会话 B
使用会话 A,我通过个人资料更新接口修改了密码。
示例请求:
```
PUT /api/profile HTTP/1.1
Host: 127.0.0.1:9000
Cookie: session=
Content-Type: application/json
{
"name":"victim1",
"email":"victim1@test.local",
"password":"VictimChanged123"
}
```
之后:
- **旧密码不再有效**
- **新密码生效**
- 但**会话 B 仍然有效**
使用会话 B 的后续请求仍然可以从 `/api/profile` 返回已认证的数据。
这证明了该问题不仅限于忘记/重置密码的路径。
它同样影响了正常的、经过身份验证的密码修改操作。
## 为什么这两个复现很重要
一个复现就已经足以说明问题了。
但在两个流程中都进行验证之所以重要,有两个原因。
### 首先
它表明该 bug **并不仅限于某一个边缘情况的恢复路径**。
相同的安全属性在以下两种情况中都失效了:
- 未经身份验证的、由恢复驱动的密码重置
- 经过身份验证的、会话内的密码修改
### 其次
它使得该问题更难被当作偶然的业务逻辑而轻易打发。
这显然是一个更广泛的会话管理弱点:
- 密码状态改变了,
- 但现有的会话仍然受信任。
这赋予了该问题更强的安全分量。
## TOTP 验证
我还在一个 **启用了 TOTP** 的账户上测试了重置流程,因为我想知道密码重置是否会悄悄削弱或绕过 2FA 的预期保护。
我确认了以下几点:
- 密码重置仍然成功
- TOTP 保持启用状态
- 使用新密码重新登录仍然会重定向到 2FA 步骤
- 所以这 **不是** 直接的 2FA 绕过
那是一个很有用的边界检查。
它正确地缩小了问题的范围。
该漏洞并不是:
- “密码重置禁用了 TOTP”
- 或 “密码重置绕过了 TOTP”
真正的问题仍然是:
- **已颁发的会话在敏感的账户安全更改后依然存活**
这是一个更清晰且更有说服力的发现。
## 严重性与分类
这个问题被合理地归类为 **高危**。
这里的关键影响是在账户安全恢复操作之后的持久性未授权访问。
advisory(安全通告)的分类如下:
- **CWE-613**:会话过期不足
- **CVSS:** `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N`
这是合理的。
这里的声明并不是说攻击者可以在没有凭证的情况下凭空登录。
这里的声明是,一旦攻击者获得了有效的已认证会话,受害者就无法通过执行本应用于恢复账户的确切安全操作(即密码重置和密码修改)来完全终止该访问。
这是一个真实且站得住脚的会话管理漏洞。
## 为什么这仍然值得报告
有些人低估了会话持久化 bug,因为他们认为会话被盗就已经意味着“游戏结束”了。
这种想法太简单了。
真正的问题是,**在**受害者注意到不对劲并采取行动**之后**会发生什么。
如果:
- 受害者重置了密码,
- 或者手动修改了密码,
- 而攻击者仍然保留着他们被盗的会话,
那么账户恢复就是不完整的。
这不仅仅是一个令人尴尬的行为。
这是恢复模型中的安全失败。
特别是在面向管理员平台中,这是一个具有高度机密性影响的有意义的问题。
## 修复分析
维护者在以下 commit 中修复了该问题:
```
db82035
```
核心的修复方向正是这个 bug 所需要的:
- 在密码重置后使旧会话失效
- 在密码修改后使旧会话失效
这是正确的补救措施,因为它针对的是真正失效的安全属性:
**当凭证更改时,旧的信任关系应当随之消亡**
解决这类 bug 的一个好方法不在于改变密码验证方式。
而在于撤销附加到账户上的、先前活跃的会话状态。
这才是能够真正恢复账户的部分。
## 漏洞披露
该问题是通过 GitHub 的安全报告流程私下报告的。
维护者:
- 审查了报告
- 接受了其作为一个安全问题
- 修复了该行为
- 并为该问题分配了编号:
**CVE-2026-34828**
在处理 advisory 期间出现的一个问题是范围。
最初的报告包括:
- 密码重置会话持久化
- 密码修改会话持久化
GitHub 最初将它们视为可独立修复的问题,以便进行 CVE 分配。
这是一个有用的提醒:即使底层的弱点在概念上是相似的,advisory 的范围界定也很重要。
最终的结果是 **CVE-2026-34828**。
## 这个 Bug 真正教会了我们什么
这里的关键教训很简单:
许多开发者的思维局限于:
- 密码的正确性
- token 的正确性
- 登录的成功
- 重置 token 的有效性
这些都很重要。
但真正的安全边界更广:
**当高风险的账户事件发生时,之前受信任的状态有哪些必须停止被信任?**
在这种情况下,答案本应是:
- 旧会话
而 listmonk 没有做到这一点。
这才是真正的核心要点。
## 关键点
- 会话撤销是账户恢复安全的一部分
- 密码重置不应让先前颁发的会话继续存活
- 密码修改不应让并行的活跃会话继续存活
- 如果恢复事件不撤销信任,会话盗窃就依然具有现实意义
- 测试多个相关流程会使报告更有说服力
- 剔除如 2FA 绕过之类的错误线索有助于保持发现的纯粹性
## 结语
这个漏洞与花哨的 payload 或巧妙的解析器技巧无关。
它只关乎提出正确的信任边界问题。
在 listmonk 中,密码更改了。
恢复操作完成了。
但攻击者的旧会话依然活着。
这就是它成为 **CVE-2026-34828** 的原因。
## 攻击链
`被盗的已认证会话 → 受害者重置或修改密码 → 旧会话仍然有效 → 攻击者在凭证恢复后仍保留账户访问权限`
## listmonk 的功能
**listmonk** 是一个自托管的邮件列表和时事通讯管理器。
它提供:
- 管理员身份验证
- 用户管理
- 活动创建
- 订阅者管理
- SMTP 和操作设置
- 基于浏览器的后台管理
这意味着它的会话模型是一个真正的安全边界。
这里的重要问题不在于 listmonk 是否支持密码重置。
真正的问题是:
**如果会话已经被盗,密码重置或密码修改真的能撤销攻击者的持久访问权限吗?**
在这种情况下,并不能。
## 为什么这个漏洞值得关注
许多安全审查过于狭隘地关注登录绕过和明显的权限提升。
这遗漏了一类重要的弱点:
**恢复失败**
如果用户更改或重置密码,该操作本应具有实际意义。
它本应降低对旧凭证和旧身份验证状态的信任度。
如果攻击者已经拥有一个有效的会话,并且该会话在恢复事件后依然存活,那么受害者实际上并没有完全恢复其账户。
这就是这里的问题所在。
这不是一个登录验证 bug。
不是加密问题。
也不是密码哈希失败。
这是一个 **会话生命周期失败**:
- 密码状态改变了,
- 账户恢复发生了,
- 但旧的会话仍然受信任。
这足以造成一个真正的漏洞。
## 我关注的边界
我并没有通过随机测试接口并指望某个接口崩溃的方式来接触 listmonk。
更有效的方法是首先识别出最高价值的信任边界。
对于重度依赖身份验证的软件,最好测试的边界之一是:
这个问题通常在以下场景中变得有趣:
- 密码重置
- 密码修改
- 2FA 变更
- 账户恢复流程
在 listmonk 中,最强烈的信号来自前两个场景。
问题正是在这里变得清晰可见的。
## 根本原因
这个 bug 并不是密码修改失败了。
这个 bug 在于 **会话比密码的有效期更长**。
从源码审查来看,密码重置流程:
- 生成并验证了一次性重置 token,
- 更新了密码,
- 创建了新会话,
但没有任何可见的旧会话撤销操作。
同样的模式也出现在经过身份验证的密码修改流程中:
- 密码被更新了,
- 但较旧的、已颁发的会话并没有被失效处理。
这种行为与实际测试的结果完全一致。
我审查的相关代码区域包括:
- `cmd/auth.go`:处理忘记/重置密码行为
- `cmd/users.go`:处理已认证用户的个人资料更新
- `internal/core/users.go`:处理密码更新逻辑
### 为什么这是可利用的
因为会话盗窃是一种真实的攻击条件。
一旦攻击者通过任何方式获取了有效的已认证会话 cookie,例如:
- 浏览器被入侵
- 恶意软件
- 共享工作站的访问权限
- 其他组件中的 XSS
- 代理或调试数据泄漏
- 意外的 cookie 暴露
受害者应该能够通过修改或重置密码来终止攻击者的持久访问。
然而在这里,他们做不到。
攻击链非常直接:
- 攻击者拥有一个有效的会话 cookie
- 受害者执行密码重置或密码修改
- 旧密码失效
- 新密码生效
- 攻击者的旧会话仍然能够成功通过身份验证
这就是整个漏洞的本质。
## 为什么这是一个安全问题,而不仅仅是应用行为
重要的区别在于恢复后的持久性。
许多应用程序将密码更改视为纯粹的凭证级别事件。
这是不够的。
真正的问题不是:
真正的问题是:
在 listmonk 中,它没有做到。
这使得原本普通的账户维护变成了不完整的安全恢复。
这就是以下两者之间的区别:
- 普通的会话连续性
- 和真正的安全弱点
## PoC
我在两个独立的流程中验证了这个问题。
### 案例 1:密码重置未撤销现有会话
首先,我创建了一个普通测试用户并登录,保存了已认证的会话 cookie。
然后我触发了忘记密码流程,捕获了重置链接,并重置了密码。
重置后:
- **旧密码不再有效**
- **新密码生效**
- 但**重置前的旧会话 cookie 仍然能够成功通过身份验证**
一个代表性的验证请求如下所示:
```
GET /api/profile HTTP/1.1
Host: 127.0.0.1:9000
Cookie: session=
标签:CVE-2026-34828, Go语言, Listmonk, Newsletter, Web安全, 会话固定, 会话持久化, 会话管理, 安全边界, 密码修改, 密码重置, 日志审计, 漏洞分析, 程序破解, 蓝队分析, 账户恢复失败, 路径探测, 邮件列表管理器