漏洞

那把钥匙还没验完,门已经开了:Rocket.Chat CVE-2026-28514

CVE-2026-28514 出现在 Rocket.Chat 企业微服务的密码登录流程:异步 bcrypt 比较返回的 Promise 尚未解析,就先被当作真值放过,随后系统签发会话令牌并把 DDP 连接绑定到被冒用的账户。

手绘风格的企业聊天值班室里,密码核验沙漏尚未落完,错误的登录请求已经越过闸门并取得会话钥匙。
文章导航

研究依据项目公告、固定提交、版本源码、运行语义与处置证据复核

来源Rocket.Chat 安全公告 / GitHub Security Lab / SOSEC 源码复核

1 凌晨两点,错误密码换回了一张有效通行证

先从一个看起来平常的夜班开始。值班工程师在监控里看见一条 Rocket.Chat WebSocket 连接,握手成功,随后完成登录。客户端没有反复试错,也没有把服务打到高负载;它只提交了一个存在的用户名和一段错误密码。照理说,bcrypt 比较结束以后,服务端应当返回拒绝。可在受影响的企业微服务部署中,拒绝没有发生。连接得到了用户 ID、会话令牌和到期时间,接着开始订阅频道。

这不是 bcrypt 算错了答案。密码散列没有被破解,比较函数最终仍会得到 false。真正的问题发生在答案回来以前:调用方拿到一个代表“稍后给出结果”的 Promise 对象,把这个对象直接送进条件判断。对象在 JavaScript 中是真值,于是程序把“核验尚未完成”误读成“核验成功”,继续走向会话创建。

Rocket.Chat 将该问题登记为 CVE-2026-28514 和 GHSA-w6vw-mrgv-69vf,定级为 Critical,归类为 CWE-287。项目公告说明,攻击者无需先登录,只要目标用户名已知或可猜,且该账户设置了本地密码,就可能用任意密码进入企业版 ddp-streamer 使用的 account service。

1.1 监控显示“登录成功”,却没有证明密码曾经通过

认证系统通常把一次登录想成连续的问答:找到账户,比较密码,依据布尔结果放行或拒绝,成功时再创建会话。这个顺序让日志很容易解释——失败计数代表密钥不匹配,成功事件代表密钥匹配。CVE-2026-28514 打乱的正是顺序。服务端仍然记录成功,也确实创建了可继续使用的状态,但促成成功的值不是 bcrypt 的布尔答案。

因此,调查不能只问“有没有异常多的失败登录”。利用路径可能只产生一次看似正常的成功,失败面板甚至保持安静。更有意义的问题是:这个成功会话来自哪个入口;当时运行的是哪条发行分支;用户是否有本地 bcrypt 字段;新会话后面紧跟了哪些订阅、消息读取或管理动作;同一来源此前是否从未使用过该账户。

这也解释了为何漏洞影响不止一个瞬时请求。登录方法返回的不是短暂的页面状态。成功分支生成原始令牌,保存对应散列,再把当前 DDP 连接标成已认证。即便最初的 WebSocket 断开,已经签发的恢复会话仍可能继续使用,实际期限由工作区的登录过期设置决定。修复代码停止新绕过,处置还要处理补丁前留下的会话。

严重性需要和适用条件一起读。NVD 给出 CVSS 3.1 9.8,CNA 给出 CVSS 4.0 9.3;两组向量都描述网络可达、无需既有权限、无需另一位用户操作,并给出高影响。可是公开材料指向的是企业版微服务登录实现,不是所有 Rocket.Chat 部署形态中的每一条认证路径。资产盘点必须先确认架构,再套用版本范围。

账户条件同样具体。源码会按用户名查询用户,并只投影 services.password.bcrypt。用户名不存在时,函数直接返回失败;账户没有本地 bcrypt 散列时,危险表达式也得不到那个真值 Promise。纯外部身份账户是否进入其他路径,需要按实际配置另行验证。项目公告用“any user with a password set”准确概括了这一点。

已知或可猜的用户名不意味着攻击者必须得到密码。用户名可能来自公开成员名片、邮件地址规则、共享频道截图、历史邀请或组织命名习惯。文章不需要假设大规模枚举,也能说明风险:一个高权限账户只要保留本地密码,且用户名落入攻击者视野,就符合公告列出的核心前提。

账户锁定和速率限制仍然有价值,却不能当作修复。一次请求就可能成功时,失败次数阈值尚未来得及触发;攻击者若已知目标用户名,也无需暴力猜解密码。临时策略可以限制来源和高权限账户的本地登录,最终仍要让密码比较结果参与判断。

防守者应把“成功登录”拆成三个可核对事实:请求声称了哪个身份,密码比较是否真的解析成 true,系统是否创建了新会话。正常实现里三者按顺序相连;受影响实现让第二项缺席,第三项仍然发生。把这三个问题分别记录,才能在类似异步缺陷出现时看见不合常理的状态转换。

1.2 漏洞只用一行代码,却跨过了整个账户的权限

“少了一个 await”很容易被讲成开发者笑话,仿佛加上一个词就结束了。可这一个词位于身份成立之前。它后面的每段代码都相信前面已经完成了密码证明:令牌生成器相信它,数据库会话写入相信它,DDP 连接相信它,后续方法和频道权限也相信它。缺陷很短,信任扩散却很长。

攻击后果由被冒用账户决定。普通成员可能只能看到自己加入的频道和直接消息;支持人员、合规审计员、机器人账户或管理员可以触及更多内容与操作。项目公告把影响写为可能导致账户接管;会话一旦按目标账户建立,能够调用哪些 DDP 方法、连接哪些频道、接收哪些消息,仍由该账户在工作区内的实际权限决定。具体工作区是否发生数据读取或配置变更,则要由本地事件证据确认。

这个区分让通报保持可信。存在受影响版本与微服务架构,足以触发紧急升级;它不能单独证明某个账户已经被冒用。公开记录也没有给出一条适用于所有工作区的网络指标,真正支持事故结论的是陌生会话与随后的敏感行为。反过来,没有失败密码峰值也不能排除利用,因为程序可能从未走到显式拒绝。

同一份技术公告还记录了另一个 account service 问题,涉及用户名查询输入并拥有独立 CVE。两者可以组合放大攻击者寻找账户的能力,但本文只复核任意密码绕过,不把组合条件写成 CVE-2026-28514 自身必需的步骤。已知或可猜用户名已经足以满足本问题的公开模型。

对于值班人员,最先要保护的是会话事实。记录服务版本、镜像摘要、工作区、用户 ID、连接 ID、来源地址、客户端特征、成功时间和后续动作;不要把原始令牌复制进工单。原始令牌是凭据,散列及其写入元数据才适合进入普通证据索引。需要撤销时使用 Rocket.Chat 支持的设备或会话管理功能。

对于开发者,最先要保护的是类型事实。一个变量若叫 valid,并不保证运行时已经是布尔值。函数签名明确返回 Promise<boolean>,调用点却让联合表达式保留了 Promise。代码审查要顺着返回类型走到条件语句,确认异步结果在进入安全决策前已经解析。

故事的第一幕于是留下两条线索:服务端确实签发了一张通行证;那把钥匙却从未被判定为正确。下一步要沿着 WebSocket 请求穿过 ddp-streamer、account service 与数据库,找到这一张通行证是怎样被逐段制造出来的。

手绘分镜展示错误密码进入异步比较后,Promise 对象先被当成真值放行,而真正的 false 结果稍后才抵达。
窄屏可横向滑动查看细节
图 1|危险点不在 bcrypt 的答案,而在程序用 Promise 对象替代答案完成了认证判断。

2 一条 WebSocket 登录,穿过了四个彼此信任的房间

Rocket.Chat 的企业微服务把原本同处一个进程的职责拆开。官方文档列出 ddp-streamer 负责 WebSocket 与 DDP 连接,accounts service 处理账户管理和登录认证,NATS 承担服务间消息,中心应用则让独立服务接管相应功能。这样的拆分便于按压力扩容,也意味着一次登录会在多个服务之间传递“这个人是谁”的结论。

请求从工作区的 /websocket 入口进入。ddp-streamer 注册名为 login 的 DDP 方法,接收恢复令牌、用户对象和密码,再调用 Account.login()。这层没有自己比较密码;它等待 account service 返回。返回 false 时抛出 403,返回结果对象时便把身份写进连接。

2.1 前台只问一次,后台却完成了两次不同的选择

Account.login() 先决定采用哪种认证方式。请求带有恢复令牌时,它转向 loginViaResume();没有恢复令牌而用户和密码均非空时,它调用 loginViaUsername();条件均不满足则返回失败。CVE-2026-28514 位于第二条分支,不影响同一文件中恢复令牌入口的选择逻辑。

进入用户名分支以后,代码按 username 查找用户,只取本次比较需要的本地 bcrypt 字段。用户不存在就立即返回 false。这一步让公开影响描述保持清晰:攻击者必须让查询命中某个真实账户,同时该账户必须保存密码散列。查询成功并不代表认证成功,它只是找到接下来要验证的材料。

第二次选择发生在密码比较之后。函数应当依据比较结果,决定返回失败,或生成并保存新会话。受影响的表达式让这里的选择失真。因为变量里放的是对象,if (!valid) 看见的永远不是“错误密码对应的 false”;它看见“存在一个 Promise”,于是跳过失败并进入令牌代码。

ddp-streamer 拿到结果对象后,连续写入三处连接状态:this.userId 获得用户 ID,this.userToken 获得散列令牌,连接对象的 loginToken 也保存同一散列。然后服务发出登录事件,并把原始令牌、到期时间和认证类型返回客户端。到这一刻,先前的异步误判已经变成其他组件可以观察和信任的已登录状态。

这条路径之所以值得逐层复原,是因为每一层单独看都像在履行职责。入口等待了 Account.login();账户服务返回了符合接口的结果;会话保存函数把令牌写进用户记录;DDP 连接依据结果标记登录。错误只在账户服务内部的一次类型转换,可下游没有理由重新比较密码。

微服务并没有制造漏洞,拆分也不是问题根源。它改变的是取证位置。入口代理可能只知道 WebSocket 升级;ddp-streamer 知道连接与登录事件;account service 知道选择了用户名分支;MongoDB 保存会话散列;中心应用和其他服务记录认证后的动作。只盯一个容器,很难还原完整故事。

安全观测因此要携带稳定的关联信息。理想记录至少能把工作区、请求或连接 ID、用户 ID、服务实例、镜像摘要和时间串起来。密码值和原始令牌不应进入日志。密码比较的最终布尔结果可以记录为脱敏状态,令牌写入可以只记录事件与散列标识的不可逆摘要,让调查既能对齐步骤,也不制造新的凭据泄露。

2.2 只有企业微服务清单命中,版本数字才开始有意义

项目公告明确写的是 Enterprise Edition 的 ddp-streamer service 所使用的 account service。官方架构文档也说明,微服务由独立容器承担 WebSocket、账户、授权与在线状态等职责;单体部署把这些功能留在一个应用进程。正文不能因为产品名称相同,就把一条 ee/apps/account-service 路径投射到所有安装方式;同样,一条 REST 登录测试通过,也不能替 WebSocket 路径完成签收,必须说明实际测试的入口与后端。

生产盘点应从部署事实开始。对于官方支持的 Kubernetes 与 Helm 方案,检查渲染后的清单和实际 Pod,而非只看一份可能过期的 values 文件。需要确认 microservices.enabled、account 与 ddp-streamer 副本、对应容器镜像、不可变摘要、命名空间、Ingress 主机和 WebSocket 路由。若使用供应商托管服务,则由平台方提供版本与修复证明。

标签本身也可能被覆盖。一个名为 7.13.3 的私有镜像不一定等于上游同名发布物;一个未写补丁号的 Helm 值也不能证明 Pod 已滚动完成。可靠盘点把运行中容器摘要映射到构建来源,再确认其中的 loginViaUsername.ts 是否含有等待操作。版本表给出目标,运行物料给出事实。

在多工作区环境里,架构可能并不统一。总部集群启用微服务,较小的隔离环境仍使用单体;灾备集群可能在几周前冻结;预发布环境可能运行 8.0 候选版。把每个工作区分别列出,能避免“主生产已经升级”掩盖仍可访问的旧入口,也能防止把不适用资产塞进紧急调查制造噪声。

网络可达性是优先级条件,不是版本免责条件。Ingress 只向内网开放可以降低外部攻击机会,却仍允许被入侵终端、VPN 用户或相邻系统访问。入口前的 WAF 也很难区分一次正常 DDP 登录和携带错误密码的绕过,因为字段形态合法。补丁必须在认证决定发生的代码里生效。

账户清单随后补上第二维。优先识别设置本地密码且权限较高、用户名容易推断、长期不登录、服务用途特殊的账户。不要导出 bcrypt 值;只需记录本地密码字段是否存在、角色、账户状态、最后正常活动和会话数量。这个最小清单足以安排验证与取证顺序。

版本、架构、账户三项合在一起,才形成可操作的暴露结论。运行受影响发行线但未启用公开路径的工作区仍要升级,却可降低即时调查优先级;启用企业微服务、入口可达且存在高权限本地密码账户的工作区应先隔离证据并补丁。每个结论都能回到一项可复核事实。

沿着这张部署地图,我们终于抵达真正出错的房间:一个返回类型写得很清楚的异步函数,和一个把对象当作答案的条件表达式。接下来要把那一行拆开,看 JavaScript 为什么会稳定地做出错误选择。

手绘路线图展示登录从 WebSocket 经过 ddp-streamer、account service、用户查询和会话数据库,再以已认证连接返回。
窄屏可横向滑动查看细节
图 2|一次登录跨越多个服务;密码判断只发生一次,下游随后把结果对象转成连接与会话事实。

3 Promise 没有撒谎,条件语句问错了对象

漏洞行可以写在一张便签上。validatePassword() 调用 bcrypt.compare() 并按类型声明返回 Promise<boolean>。用户名分支先确认散列存在,再把调用结果赋给 valid,接着执行 if (!valid)。代码的意图一眼可见,运行时含义却少了一步。

bcrypt 比较需要异步完成。调用函数时返回的 Promise 是一个容器,表示未来会有布尔答案;它自己不是那个答案。错误密码让容器最终装入 false,正确密码最终装入 true。如果程序不等待,条件判断只能看见同一种东西:一个已创建的对象。

3.1 逻辑与运算保留了对象,取反运算又替它盖了章

受影响表达式使用逻辑与:先计算本地 bcrypt 散列,再计算 validatePassword()。当散列存在并为真值时,JavaScript 的 && 返回第二个操作数本身,不会强制把它解析成布尔值。因此 valid 得到 Promise 对象,类型语义已经偏离变量名。

下一行的逻辑非会执行布尔转换。ECMAScript 的 ToBoolean 规则只把少数原始值视为假:例如 undefinednull、零、NaN 和空字符串;普通对象最终返回真。Promise 是对象,无论处于 pending、fulfilled 还是 rejected,其对象真值都不由未来结果改变。

于是错误稳定发生,不依赖竞态。即使 bcrypt 极快,在同一个同步表达式里,调用仍先返回 Promise;没有 await,程序不会把已完成状态中的值自动取出来。即使最终比较为 false,条件语句早已对 Promise 对象取反并得到 false,拒绝分支不会执行。

这一点对复现和防守都重要。它不是“偶尔在高并发下发生”的时序窗口,也不是硬件性能、线程调度或散列成本参数造成的概率事件。只要查询命中带本地散列的账户并走到该表达式,语言规则就给出同样结果。降低并发、提高 bcrypt 成本或重启服务都不会修复判断;Boolean(promise) 或双重取反也只会更明确地把对象转成 true。

异常拒绝也不是主要故事。Promise 最终解析为 false 时不会抛错,因此未必出现未处理拒绝告警;程序只是忽略了一个正常完成的错误答案。若 bcrypt 本身抛错或 Promise 被拒绝,认证则必须通过既有错误处理安全失败,并在令牌创建前终止。测试既要覆盖正常的 false,也要确认异常、超时和服务失败不会形成临时身份。

变量的运行时形态可以在测试里直接断言。错误实现中,当账户有散列时,valid 的值具有 Promise 行为;修复后它应是布尔值。更稳妥的测试不窥视局部变量,而观察安全结果:错误密码必须返回 false,不得调用令牌生成器,不得调用 saveSession(),也不得让入口发出登录事件。

普通 TypeScript 编译未必拒绝放在真值位置的 Promise,因此还需要类型感知的 lint 检查业务决策。typescript-eslint 的 no-misused-promises 针对 Promise 进入条件语句,no-floating-promises 则覆盖被直接丢弃、连同异常和顺序一起失去的返回值。认证包应同时启用两类规则及类型信息。

3.2 一个等待操作,把认证重新排回正确顺序

上游修复把表达式改为等待密码比较,再让逻辑与返回布尔结果。对于存在散列的账户,await validatePassword(...) 先得到 truefalse;错误密码使 if (!valid) 立即成立,函数在生成令牌以前退出。正确密码继续进入原有成功流程。

修复看似只增加括号与 await,它恢复的是一项顺序约束:凭据证明完成以后,才能创建身份状态。这个约束应当写进测试和审查准则,而非依赖某个变量恰好命名为 valid。任何返回 Promise 的认证、授权、风控或账户状态检查,都要在安全动作前解析并处理失败。

调用链中已有其他 await,这也是缺陷容易被忽略的原因。ddp-streamer 会等待 Account.login(),用户名函数会等待数据库查询和 saveSession()。外层函数整体是异步的,并不意味着内部所有异步结果都会自动完成。每个调用点仍要明确决定等待、返回、并发聚合或放弃。

代码评审可以用“第一个不可逆动作”帮助定位。这里的第一个高价值动作是生成并保存新会话。评审者从该动作向上追,要求所有身份条件在到达它之前成为已处理的原始值:用户存在、账户允许登录、密码结果为真。若其中任何一项仍是 Promise、回调句柄或未确认响应,顺序就没有闭合。

回归矩阵至少包含五类:不存在的用户名;存在用户但无本地散列;存在用户且密码错误;存在用户且密码正确;已有恢复令牌。前三类不得生成新密码会话,第四类应生成且可使用,第五类应保持原本恢复语义。再用一条测试故意移除等待操作,确保安全用例必然失败,才能证明测试真的覆盖本缺陷。

端到端验证还要穿过 DDP 入口。单元测试能证明用户名函数返回值,无法单独证明入口没有把其他真值误当成功。使用专门测试账户,在隔离环境提交已知错误密码,预期收到拒绝;随后提交正确密码,预期登录;最后验证受控恢复令牌。记录状态码、登录事件与会话写入数量,不保存凭据内容。

静态规则也需要类型信息。若代码把异步函数的返回类型弱化成 any,规则可能失去判断依据。认证模块应保留具体的 Promise<boolean>false | ILoginResult 等签名,并避免用宽泛类型掩盖安全分支。类型系统不能替代运行测试,却能让错误对象难以悄悄进入条件。

现在那一行已经解释清楚:bcrypt 会给出正确答案,Promise 会忠实交付答案,出错的是程序在答案抵达前就做了决定。下一幕要看这个过早决定怎样从内存里的真值,变成数据库里可恢复的会话和连接上的用户身份。

手绘时间轴对比缺失 await 时判断先于 false 结果发生,以及修复后程序等待布尔答案再决定是否放行。
窄屏可横向滑动查看细节
图 3|两条路径只差一次等待;安全差异来自判断和会话创建的先后顺序。

4 错误判断落地以后,变成了真正可以续用的身份

如果漏洞只让一个局部变量暂时为真,影响会在函数返回时消失。Rocket.Chat 的成功分支做了更多事情。它调用 _generateStampedLoginToken() 生成带时间的原始令牌,再通过 _hashStampedToken() 得到散列形式,随后等待 saveSession() 把散列写进用户文档。

saveSession() 的代码很短:按用户 ID 更新记录,把新散列追加到 services.resume.loginTokens。原始令牌留给客户端,数据库保存用于后续验证的形式。这样设计避免在数据库中直接存放可立即使用的凭据,却也意味着一次错误认证会形成持久状态,不能靠断开最初连接自动清除。

4.1 从一张原始令牌,到数据库和连接上的三份痕迹

用户名函数返回的结果对象包含用户 ID、原始令牌、散列令牌、到期时间和类型 password。这不是“密码看起来正确”的提示,而是完整登录产物。ddp-streamer 接收以后,把用户 ID和散列令牌写入当前服务对象及连接,再发出两次登录事件:一次面向当前连接,一次面向服务器事件总线。

这些状态给调查提供了不同视角。客户端侧可能留下原始令牌或设备会话;数据库侧能看到用户的恢复令牌散列集合发生变化;ddp-streamer 侧能看到连接从匿名变成某个用户;后续服务能看到该用户调用方法或订阅频道。单个视角可能因为日志轮转、隐私设置或版本差异而缺失,多处对齐能提高结论强度。

原始令牌不适合在调查中广泛收集。复制它会扩大会话被继续使用的风险。更安全的证据包保存令牌创建时间、用户 ID、会话或设备标识、散列存在性、首次和最后活动、来源网络信息以及撤销结果。若必须接触原始值,应按凭据级别隔离、限制访问并尽快使其失效。

数据库记录可以帮助弥补入口日志的短保留期,但不能单独证明漏洞利用。合法登录同样会添加恢复令牌。需要把新增时间与陌生来源、运行版本、DDP 登录事件、客户端特征和随后的行为连接起来。若用户刚更换设备并完成正常登录,同一数据库变化会有完全不同的解释。

连接事件也要谨慎解读。一次 WebSocket 101 只说明协议升级成功;它发生在认证之前。真正有价值的是 DDP login 方法返回成功、连接绑定 user ID、登录事件发出以及后续调用。代理层如果无法看见 DDP 消息内容,应依靠应用侧结构化事件,避免把所有长连接都当作风险。

会话到期不是固定不变的常数。Account 服务代码有初始值,同时会读取 Accounts_LoginExpiration 设置并监听变化。调查时间窗应依据工作区配置、最近变更和实际会话记录决定。若配置允许较长会话,补丁上线以前创建的令牌可能在更晚时间继续出现活动。

“补丁完成”与“会话清理完成”因此是两张工单。升级让新请求得到正确密码判断;撤销可疑会话消除已经签发的身份;密码重置处理攻击者可能获得的其他凭据;行为审计评估被冒用账户做过什么。把四项合在一个勾选框里,会让团队在代码已经安全时遗留旧令牌。

4.2 登录后的世界,完全按照受害账户的角色展开

一旦连接拥有 userId,后续 DDP 方法与频道订阅会按该账户的权限处理。漏洞没有直接给所有攻击者同一组管理员能力;它让攻击者成为被点名的账户。普通成员、频道所有者、审计角色、集成机器人和系统管理员的后果因此相差很大。

公开的受控验证显示,成功以后可以与可用 DDP 方法交互、连接频道并接收消息。这足以证明认证状态转换具有实际作用。公开报告没有替每个组织列出可读频道、管理权限、应用令牌或外部集成。部署方应从角色与房间成员关系还原实际可达范围。

高权限不只等于管理员。值班机器人可能能读取告警频道;合规账户可能能检索历史消息;支持主管可能能访问客户对话;自动化用户可能持有外部 webhook 或应用权限。资产清单应按“被冒用以后能做什么”排序,不能只筛选角色名包含 admin 的账户。

消息机密性也受房间类型和加密设置影响。服务端认证成功不自动等于能解密所有端到端加密内容;实际客户端密钥状态另有机制。本文不把账户接管扩大为对所有历史密文的普遍读取。调查应依据频道、订阅、客户端密钥和具体事件分别下结论。

完整性影响来自身份可执行的动作。攻击者可能以该用户发送消息、修改个人设置、加入允许加入的房间,或在权限足够时调用管理方法。是否实际发生,需要查看方法调用、消息创建、角色或设置变化。仅看到新会话时,应报告“未经授权的认证状态”,再随证据增加更新影响。

可用性影响也取决于后续动作。CVSS 评分给出高可用性影响,是对潜在账户能力和产品风险的总体估计;它不是某个工作区已经停机的证据。内部通报可以同时保留产品评分与本地事实,例如“高权限账户会话已确认,未发现服务中断”,避免二者互相覆盖。

时间关联是从会话走向行为的桥。对每个可疑登录,以创建时间为起点,检查随后几分钟和整个令牌寿命内的频道订阅、消息读取、发送、文件访问、权限变更、设备登记和 API 活动。若令牌从多个地理位置或客户端重复出现,记录每次活动,不要只保留首个来源。

至此,凌晨的那条登录已经有了完整后果:错误密码没有通过,程序却创建了会话;会话让连接获得真实用户身份;身份再打开该用户原本拥有的房间和方法。接下来要回到仓库历史,确认哪次改动恢复了顺序,以及不同发行分支从哪个版本开始安全。

手绘编排图展示错误判断之后的令牌生成、散列保存、DDP 连接绑定、登录事件和后续频道活动。
窄屏可横向滑动查看细节
图 4|漏洞的影响在成功分支里逐步落地:内存中的真值变成数据库会话、连接身份与后续操作。

5 修复只有一个等待操作,发布却分布在七条支线上

公开公告展示的漏洞源码固定在提交 05c415b9…,其中危险表达式仍然存在。沿文件历史追踪,真正加入等待操作的是提交 dd7032a0a4c25b6be5b3af05c83f8d07d5102424,提交说明为“ddp streamer not waiting some requests completion”,时间是 2026 年 1 月 12 日。认证文件的功能性差异只有一行。

一行修复并不等于只核对一条主分支。Rocket.Chat 同时维护多个 7.x 发行线,并在 8.0.0 发布修复。项目 GHSA 列出的安全下限是 7.8.6、7.9.8、7.10.7、7.11.4、7.12.4、7.13.3 与 8.0.0。工作区需要与自己所在发行线比较,不能拿 7.13.3 去判断 7.10 部署。

5.1 代码差异恢复了顺序,构建来源证明差异真的进入运行物

修复后的表达式先等待 validatePassword(),再让逻辑与返回布尔值。错误密码得到 false,下一行退出;正确密码得到 true,后面的令牌代码不变。这个最小改动降低了旁路回归风险,也让验收目标非常明确。

提交本身还包含 changeset 和持续集成工作流变化。因果判断要落在认证文件的一行差异上,不把同一提交中的其他文件都描述成安全修复。上游 7.13.3 标签对应提交 1864c260…,其用户名登录源码已经包含等待操作;8.0.0 发布页则记录主版本在 1 月 12 日发布。

版本管理团队应保存三类证据:供应商公告说明哪个版本修复;发布标签或固定提交说明源码状态;内部镜像摘要说明哪个构建正在运行。只保存网页截图会在镜像重建、标签覆盖或私有补丁出现时失去可追溯性。三者互相指向,才能回答“我们实际上部署了什么”。

对私有 fork,字符串搜索可以快速初筛,完整判断仍需看调用语义。代码中出现 await validatePassword 是好信号;若包装函数更名、返回类型改变或调用移位,要确认最终条件拿到布尔值,并且失败发生在任何令牌动作之前。补丁搬运不能只复制一行而忽略本地差异。

构建流水线还应记录源提交、依赖锁、编译配置和产物摘要。CVE 根因不在 bcrypt 版本,盲目升级依赖不会替代源码修复。相反,应用版本看似达到安全下限但使用了旧企业服务镜像,仍可能保留危险路径。微服务集群要逐个核对 account service 与 ddp-streamer 的发布状态。

滚动升级期间可能短暂混跑新旧 Pod。若流量仍能落到旧 account service,一部分登录继续受影响。验收需等待旧副本完全退出,确认 Deployment/StatefulSet 的 desired、updated、available 数一致,再从每个运行摘要抽样验证。回滚仓库也要删除或标记不安全镜像,避免故障时恢复到旧版本。

发布说明的日期与公开披露日期不同。修复先进入版本,GHSA 于 3 月 5 日发布,技术细节于 3 月 12 日公开。这种协调披露让用户先获得补丁。调查时间窗不能只从公告日开始;任何受影响版本实际运行且入口可达的时期都属于需要评估的历史。

5.2 七个安全下限,要在资产表里变成七条明确规则

最稳妥的版本判断是按次要版本分组。7.8 系列低于 7.8.6 受影响;7.9 低于 7.9.8;7.10 低于 7.10.7;7.11 低于 7.11.4;7.12 低于 7.12.4;7.13 低于 7.13.3。8.0 的公开候选版位于 8.0.0 正式版之前,应按受影响处理;8.0.0 是该线列出的修复起点。

发行线首个公告修复版资产判断示例验收重点
7.87.8.67.8.5 及更早命中确认企业 account service 产物含等待操作
7.97.9.87.9.7 及更早命中确认旧 Pod 全部退出
7.107.10.77.10.6 及更早命中错误密码不得创建会话
7.117.11.47.11.3 及更早命中正确密码与恢复登录仍正常
7.127.12.47.12.3 及更早命中镜像摘要与上游来源一致
7.137.13.37.13.2 及更早命中优先替换该支线的 7.13.2 及更早版本
8.08.0.0rc0 至 rc5 命中评估主版本升级前置条件

多条件版本字符串不能简单做字典序比较。资产平台应解析语义版本,识别预发布标识,再按发行线应用下限。把 7.9.10 当作小于 7.9.8 或把 8.0.0-rc5 当作高于正式版,都会产生错误结果。

版本已到修复下限的资产仍需验证。私有构建可能从较早源码派生,供应链缓存可能复用旧层,部署状态可能没有完成。相反,少数私有 fork 也许提前搬运了补丁;这种例外要由固定源提交、代码差异和运行测试共同证明,不能靠口头说明从清单删除。

8.0.0 是大版本,官方发布说明包含数据库与弃用功能变化。不能为了赶安全修复而跳过升级准备。仍在 7.x 支持线的工作区可以先上对应补丁版;计划进入 8.x 的团队按官方迁移要求完成备份和兼容检查。安全紧迫性要求缩短窗口,不要求忽视发布工程。

公告列出的安全下限用于快速判断暴露,不是长期停留目标;后续受支持发布原则上应继续继承修复。对于已结束支持的旧版本,应选择受支持的更高版本,并在过渡期降低入口暴露、收紧高权限本地密码账户、加强会话监控。临时控制只能降低机会,不能改变错误代码的运行语义。

升级记录应包含开始与结束时间,因为调查需要知道旧 Pod 最后一次可接收登录的时刻。把这个时间与会话创建、Ingress 流量和镜像终止事件对齐,可以区分补丁前风险、滚动期间风险和补丁后验证流量。只记录“某天已升级”不够精确。

七条发行线最后收敛成同一个验收事实:错误密码得到拒绝,且数据库没有新增恢复会话。版本号帮助找到应有代码,测试证明运行中的代码确实遵守它。下一步就是把这个事实放进生产变更,从部署清单一直验证到每个副本。

手绘代码修理台把缺失的 await 放回密码比较之前,并在令牌生成器前恢复拒绝闸门。
窄屏可横向滑动查看细节
图 5|补丁改动很小,验收目标却覆盖源码、构建、滚动副本和会话副作用。

6 把补丁带进集群,还要证明每一扇门都换了锁

真正的修复工作从版本清单开始,却不会在镜像拉取完成时结束。Rocket.Chat 官方说明,微服务部署由 Kubernetes 与官方 Helm chart 支持,account、ddp-streamer、authorization、presence 和 NATS 可分别设置副本。任何一个仍接收认证流量的旧 account service,都会让集群保留不一致的结果。

变更负责人需要同时控制配置和运行状态。先保存 Helm values、渲染清单、当前镜像摘要与副本拓扑;再部署供应商修复版;等待旧 ReplicaSet 清空;最后从真实入口做认证验收。这样失败时可以定位是选错版本、镜像未更新、流量仍到旧副本,还是测试账户配置不符合预期。

6.1 资产盘点从摘要出发,沿着服务发现走到真实 Pod

第一张表按工作区列出入口主机、命名空间、Helm release、chart 版本、Rocket.Chat 应用版本、account 镜像摘要、ddp-streamer 镜像摘要、副本数、数据库集群和 NATS 端点。镜像 tag 作为人类可读标签保留,摘要作为不可变标识用于判断。运行信息从 Kubernetes API 导出,不只依赖 Git 仓库中的期望配置。

第二张表记录流量路径。Ingress 或网关将 /websocket 交给哪个 Service,Service 选择哪些 Pod,ddp-streamer 如何调用 accounts,是否存在多集群、灰度、灾备或区域路由。漏洞入口能否命中某个旧副本,由实际服务发现决定。一个遗忘的 canary 也可能接收少量登录。

第三张表记录账户条件,但只保存必要元数据。对高权限与敏感角色,确认是否存在本地密码散列、是否启用、最近正常登录、活动设备数量和负责人。若组织要求强制 SSO,却仍有历史本地密码字段,这次盘点也是清理机会。不要为了调查导出散列值本身。

升级前保存可回滚配置,不保留不安全镜像作为默认回滚目标。若新版本出现业务问题,应回到包含安全修复的上一可用构建,或采用经审计的向后移植。把旧镜像从自动回滚策略、灾备模板和离线镜像仓库的“推荐”标签中移除,避免几周后再次上线。

升级期间限制管理账户使用可以减少新会话噪声,也便于对比。必要时短暂收紧 WebSocket 入口来源或把高权限本地密码账户停用,但应评估业务连续性。临时措施需要写明开始、结束和负责人,补丁成功后撤销,不能成为长期无人维护的网络例外。

每个 Pod 的启动时间与镜像摘要要进入变更证据。只看到 Deployment 指向新 tag 还不够;节点镜像缓存、拉取策略和未完成滚动都会造成实际差异。使用支持的镜像签名或制品证明时,把验证结果和源提交关联,减少供应链层面的不确定性。

数据库无需为修复做手工改写。漏洞逻辑在应用认证调用,项目公告的处置是升级到修复版本。会话撤销属于事故响应,不应通过临时脚本直接修改用户文档;使用产品支持的控制可以触发相应清理并保留一致性。

6.2 一组小而硬的验收用例,分别守住失败、成功与恢复登录

选择专门测试账户,不使用真实管理员。账户设置已知本地密码,权限限制在无敏感数据的测试频道。测试前记录其活动会话并清理无关设备,让新增会话数量可以准确观察。所有请求从正常外部入口进入,以覆盖 Ingress、ddp-streamer、account service 和数据库。

  1. 错误密码。提交存在用户名和已知错误密码,预期 DDP 登录拒绝。验收不仅看客户端错误,还检查没有新增 services.resume.loginTokens、连接没有 user ID、没有登录事件、没有后续订阅成功。任何一个副作用出现,都说明运行路径仍不安全或观测存在矛盾。
  2. 正确密码。提交正确密码,预期成功并创建一条新会话。它证明升级没有把密码登录整体破坏。记录创建时间与设备条目,然后立即通过支持的方式注销该测试会话,防止验收凭据长期存留。客户端响应中的原始令牌不写入普通测试报告。
  3. 有效恢复令牌。使用受控的有效恢复令牌,验证恢复登录仍按预期工作。
  4. 无效或已撤销恢复令牌。验证请求被拒绝。CVE 修复位于用户名密码分支,完整验收仍要确保相邻分支没有因版本升级出现业务回归。
  5. 不存在用户或没有本地散列。覆盖“存在用户但没有本地 bcrypt 散列”与“不存在用户”。两者都应拒绝且不创建会话。响应内容不应过度泄露账户差异;无论产品如何统一错误提示,内部结构化事件可以记录分支原因,访问权限仅给安全和运维人员。

矩阵在每个运行摘要上执行,至少让调度覆盖所有 account service 副本。若服务层无法定向 Pod,可临时用可审计的测试路由、逐副本端口转发或滚动观测来确认;生产用户不应承担随机命中旧实例的风险。测试方法写进变更方案并在结束后清理。

验收需要负向对照。让同一测试账户在升级前的隔离副本上重现“错误密码产生会话”,再在修复副本上得到拒绝,可以证明用例确实触达目标分支。对照环境与生产隔离,测试结束删除数据和令牌,不把漏洞副本留作长期服务。

同时观察账号服务错误和延迟。若 bcrypt 调用异常、数据库不可用或服务间超时,认证必须安全失败,不能把异常对象或部分结果送进成功路径。CVE 修复聚焦缺失等待,恢复测试可把这些失败模式一并纳入,强化同一“未证明就不创建身份”的原则。

性能与监控结果只说明变更是否健康,不能替代安全验收。错误密码应增加拒绝事件但不增加会话,正确密码应同时出现成功和会话写入;延迟和吞吐可帮助发现部署问题,却不能证明错误密码已被拒绝。

最终签收物应能被另一位工程师重放:固定版本和摘要、测试账户条件、入口、请求时间、预期与实际状态、会话计数和每个副本的覆盖。成功与失败还应能用同一关联 ID 穿过代理、ddp-streamer、account service 与会话记录,且任何字段都不泄露密码或令牌。

完成后清理测试会话、临时路由、调试级别和额外权限,并由第二位工程师复核。候选回滚构建也必须通过同一错误密码用例,否则不能成为稳定、灾备或紧急回退版本。安全测试留下的入口和未证明的回滚制品,都可能把刚关上的门重新打开。

当所有副本都拒绝错误密码且没有会话副作用,新的锁才算换完。可调查的历史不会随之消失;下一章从补丁前的会话与行为出发,区分普通登录、自动重连和真正可疑的身份接管。

暖纸手绘示意图以四条代表性支线和四个加锁修复包,概括 Rocket.Chat 多条维护线分别进入安全下限、运行镜像摘要和副本验收的过程。
窄屏可横向滑动查看细节
图 6|四条示意支线概括多线维护,具体七个安全下限以表格为准;版本号最终要落到运行摘要和每个副本的认证验收。

7 真正的调查,从“成功登录”之后才开始变得清晰

补丁回答“现在还能不能发生”,历史调查回答“以前有没有发生”。CVE-2026-28514 最麻烦的地方,是利用结果长得像成功登录。单条成功事件没有异常语法,单条令牌写入也是正常功能。调查价值来自多个独立记录在同一时间和身份上相交。

先定义调查窗口。起点是该工作区首次运行受影响企业微服务版本且入口可达的时间;若无法精确确定,从可用部署历史最早处开始。终点至少延伸到所有旧副本退出、错误密码验收通过并完成可疑会话撤销以后。公告发布日期不是天然起点。

7.1 五段证据拼在一起,才像一场未经授权的登录

第一段是入口事实:来源地址、受信代理处理后的客户端 IP、WebSocket 路径、连接时间、用户代理和请求速率。入口日志通常看不到密码内容,也不应记录密码。它能说明谁在何时建立连接,不能单独说明 DDP 登录结果。

第二段是应用认证事实:连接 ID、被声明的用户名或解析后的用户 ID、账户服务分支、成功或拒绝、服务实例与构建摘要。若现有版本没有结构化字段,优先使用可用日志和审计能力,不在生产临时打开会泄露敏感载荷的调试记录。未来版本可补充最小事件。

第三段是会话事实:用户记录何时新增恢复令牌散列,设备管理中出现何种客户端,令牌何时最后活动或被撤销。数据库快照或变更审计必须按组织流程取得。散列本身无需在多个系统复制;保留不可逆指纹、数组变化与时间已足够关联。

第四段是认证后行为:频道订阅、历史读取、消息发送、文件访问、设置改变、角色操作、应用调用和管理 API。先按被冒用账户平日行为建立对照,再看新会话是否在短时间内进行异常广泛或高敏感动作。权限越高,审计范围越广。

第五段是用户与终端确认。询问账户所有者在对应时间是否换设备、使用 VPN、升级客户端或从新地点登录;核对端点安全记录、浏览器会话和公司身份系统。用户确认能排除大量合法变化,却不能替代服务器证据。高风险事件即使用户不确定,也继续按技术记录调查。

这五段形成强度梯度。只有 WebSocket 握手是弱线索;握手加成功登录和新会话是较强线索;再加陌生来源与敏感操作,可以支持未经授权使用结论。相反,版本命中但没有保留任何相关日志,只能报告无法排除,不能写成“未发现即未发生”。

查询时优先用状态序列,少依赖单个阈值。例如:陌生来源建立连接;高权限用户产生新密码会话;同一连接立即订阅多个私有频道;随后读取历史或执行管理方法。这个序列比“每分钟失败十次”更贴合漏洞,因为绕过可能一次成功。

时钟误差会破坏序列。统一转换为 UTC,记录各系统原始时区和已知漂移;Kubernetes 节点、数据库、代理与应用日志若相差几十秒,使用连接 ID、用户 ID和会话创建时间共同对齐。调查报告注明校正方法,避免把顺序画得比证据更精确。

透明的线索评分可以帮助排序:受影响摘要、陌生来源、新会话、高权限账户与后续敏感动作会提高可信度,用户确认的设备变化、已知企业出口和计划中的客户端升级则可能降低疑点。每个因素都应与原始证据并列,分数只能辅助分析,不能替代分析。

日志本身也要接受凭据安全检查。若应用或网关记录了完整 DDP 参数,应确认其中是否混入密码或令牌,并把相应日志视为敏感存储。长期事件结构使用字段白名单,只保留方法、结果、用户、连接、服务版本和关联 ID,不能为调查一处凭据缺陷再制造另一处暴露。

7.2 没有完整日志时,用会话库存和行为差异缩小未知

很多工作区不会保留数月 DDP 细粒度日志。此时从当前活动会话和数据库中仍可用的令牌元数据开始,列出创建或首次观察时间、最后活动、设备信息、来源和用户确认。对长期有效且来源陌生的会话先撤销,再保存撤销前的最小元数据。

客户端设备列表可以提供线索。Rocket.Chat 官方账户设置文档说明,用户可查看已登录设备及最近登录活动,并远程注销设备,也可注销其他登录位置。管理员处置要遵循权限和审计流程;对高风险账户,通知所有者核对列表能迅速发现明显陌生设备。

代理日志保留较长时,可以从成功会话的首次活动向前寻找对应 WebSocket 连接。即使看不到 DDP 方法,来源地址、时间与持续连接长度仍能补全环境。注意共享 NAT、企业代理和移动网络会让多个用户拥有相同外部 IP,不能把地址直接等同于个人。

消息与管理审计也能反向定位会话。发现异常设置变更或陌生消息时,按用户和时间回查连接与会话。对于端到端加密房间,保留服务器可见的元数据,不尝试绕过加密收集内容。取证需要满足法律、隐私与员工政策。

如果日志不足以判断令牌是否由漏洞创建,风险决策可以依据账户敏感度与令牌年龄。高权限、长期有效、来源未知的会话适合主动撤销;普通账户也可在用户可接受的时间强制重新登录。撤销造成的业务成本通常低于让未知身份继续存在,但操作仍应由组织负责人批准并通知用户。

调查中不要验证真实生产账户的任意密码绕过。公开证据已经确认机制,生产重现会创建新的未经授权会话并污染证据。动态验证放在隔离副本和专用测试账户;生产只做版本、状态与历史证据读取。若必须进行更深验证,另行制定书面方案、最小权限和清理步骤。

发现相邻异常时单独记录。同一份技术公告描述了另一个用户名查询问题,拥有独立 CVE;日志里若出现结构化用户名输入或查询异常,不应塞进 CVE-2026-28514 的结论。分别建立时间线和处置项,组合影响也需要证据支持。

CISA 的公开 SSVC 条目在 2026 年 3 月 10 日把 exploitation 记为 none,表示当时的公开信息没有已知利用;它不是某个私有工作区的取证结论。这个背景可以进入报告,但本地排查仍须以自己的部署和日志为准。公开世界尚无已知利用,既不会修好一个可达的严重漏洞,也不能回答组织内部曾经发生过什么。

调查结束要明确三种结果:确认未经授权会话及其活动;发现高可信可疑序列但证据不足以确定全部影响;未发现相关活动但因保留不足无法完全排除。每种结果都写明查看了哪些数据、缺少哪些数据和已采取何种控制,避免一句“无异常”掩盖可见性限制。

当会话、连接和行为终于在时间线上对齐,凌晨那次“成功登录”才不再只是一个普通事件。最后的任务是安全地撤销身份、恢复账户、评估消息与设置影响,并把这种异步误用从下一次代码提交中挡住。

手绘调查桌把入口连接、认证事件、会话散列、设备清单、频道动作和用户确认按时间拼接。
窄屏可横向滑动查看细节
图 7|利用结果伪装成成功登录,只有跨入口、应用、数据库、行为与用户确认的关联才能提高结论强度。

8 让错误会话失效,也让下一次 Promise 无法冒充答案

处置顺序要同时照顾正在发生的风险和证据。先保存最小必要的部署、连接、会话与行为元数据;随即把所有认证流量切到修复副本;对确认或无法合理解释的会话执行撤销;再重置相关本地密码、复核外部身份和账户状态;最后评估该身份在有效期间触及的频道、消息、文件与管理动作。

只改密码并不充分。漏洞签发的是恢复会话,已创建令牌可能独立于后续密码变化继续工作,具体取决于产品会话管理。Rocket.Chat 提供设备查看、注销设备和注销其他登录位置的功能,管理员还需按本地权限与流程处理其他用户会话。撤销结果应进入事件时间线。

8.1 恢复工作区时,先收回身份,再核对这个身份做过什么

确认绕过后,首先冻结可能被覆盖的日志和数据库元数据,记录当前会话清单,然后撤销受影响用户的活动会话。若攻击仍在进行,可临时停用账户或限制 WebSocket 入口。措施要有时间、批准人与恢复条件,避免紧急控制变成长期不可见的配置。

随后重置本地密码,并检查同一账户是否绑定 SSO、API token、个人访问令牌、应用凭据或机器人集成。CVE 本身不证明这些凭据已经泄露;若会话有能力创建、查看或使用它们,就按行为证据和可见性决定轮换。优先处理管理员、集成与合规角色。

内容影响按账户权限和实际动作调查。列出会话期间加入或访问的房间、读取历史、下载文件、发送或编辑消息、修改设置、角色变化与外部调用。向业务负责人提供事实与未知,不把产品级最高影响直接写成每个工作区的既成损失。

用户通知要给出可执行信息:异常时间、受影响设备或会话、已完成撤销、是否需要重新登录、密码或第二因素处理、需要报告的可疑消息。不要在通知里暴露其他用户、内部 IP 或令牌。若涉及受监管数据,按法务与合规流程评估报告义务。

服务恢复以后继续观察一段覆盖会话过期和客户端重连的窗口。关注被撤销令牌重复使用、同一来源改换多个用户名、旧镜像重新出现、账户从陌生设备再次登录,以及补丁后错误密码测试意外成功。每个告警都应带运行摘要,防止灰度回滚把问题带回来。

事件复盘需要回答为何类型检查、lint、单元测试和端到端验收没有拦住这一行。目标不是追究“谁忘了一个词”,而是建立可重复的控制:Promise 进入条件时报错;错误密码路径断言无令牌副作用;认证服务变更必须经过安全用例;发行分支同时获得补丁;生产签收覆盖所有副本。

供应商公告建议使用严格 TypeScript 配置并捕获未等待 Promise。实际规则选择要准确:条件位置由 @typescript-eslint/no-misused-promiseschecksConditionals 直接覆盖,独立未处理的异步语句由 no-floating-promises 覆盖。两条规则都需要类型信息,认证包不应通过关闭类型检查换取短暂构建速度。

测试也要观察副作用顺序。将令牌生成器、saveSession() 和登录事件作为探针:密码比较返回 false 时,它们的调用次数必须为零;返回 true 时按顺序发生。这个断言比“函数返回了 false”更强,因为它防止未来重构在返回前已经留下会话。

8.2 回到凌晨两点,修复后的门会等到答案真正抵达

现在重新播放开场。相同的测试账户,相同的错误密码,相同的 WebSocket 入口。ddp-streamer 把请求交给 account service;用户查询命中本地散列;validatePassword() 返回 Promise;修复代码停在这里等待。bcrypt 得到 false,用户名函数返回失败,入口给出拒绝。

更重要的是没有发生什么。令牌生成器没有运行,用户文档没有新增恢复会话,连接没有绑定 user ID,服务器没有发出已登录事件,频道订阅也没有继承目标账户权限。安全验收应把这些“未发生”变成可观测断言,避免只看一个客户端提示。

换成正确密码,比较解析为 true,原有成功流程继续:创建令牌、保存散列、绑定连接、返回会话。修复没有取消密码登录,也没有靠禁用企业微服务规避问题。它让放行与密码答案重新一致,这正是认证功能应有的行为。

这起漏洞提醒我们,异步代码的安全问题常常不是异常,而是错误的正常值。Promise 没有 rejected,bcrypt 没有报错,服务没有崩溃,接口甚至返回结构完整的成功响应。只有顺着状态变化追踪,才会发现成功缺少了应先完成的证明。

开发规范可以把这项经验写成一句可测试的规则:任何建立身份或权限的持久动作,都必须位于所有相关异步检查成功解析之后。检查结果使用明确布尔类型;失败路径断言没有令牌、会话、事件和下游调用;静态分析拒绝 Promise 参与条件。

运维规范则对应另一句:版本命中触发升级,状态证据决定事故结论。微服务架构、发行线、运行摘要和账户条件确定暴露;连接、会话与行为确定是否发生未经授权使用;日志不足时诚实记录未知,并用撤销与密码恢复缩小剩余风险。

公开来源没有报告已知真实世界利用,CISA 的 SSVC 记录也在 2026 年 3 月 10 日标为 exploitation none。这个事实不能降低修复优先级:入口无需认证,攻击复杂度低,成功后获得真实账户会话。它只约束公开表述,不能替本地工作区做历史结论。

对管理者,最短行动清单已经很具体:确认是否部署企业 account 与 ddp-streamer 服务;按发行线升级到修复版或更高受支持版本;验证每个运行副本拒绝错误密码且不写会话;清点高权限本地密码账户;关联补丁前会话与后续活动;撤销无法解释的身份。

对维护者,长久成果也很具体:在认证包启用类型感知规则;保留精确返回类型;把失败副作用写进测试;让安全补丁进入每条受支持发行线;把源提交、镜像摘要和验收结果连在一起。这样下一次异步函数被重构时,流水线会在门打开前要求答案。

凌晨两点的连接最终回到它应有的位置。错误密码仍然会让 bcrypt 得到 false,程序也终于等到了这个 false。没有令牌,没有新会话,没有借来的身份。门没有依靠运气关上;它只是恢复了最朴素、也最重要的顺序:先证明,后通行。

手绘处置台展示保存证据、升级所有副本、撤销会话、重置凭据、审计行为和加入异步回归测试的顺序。
窄屏可横向滑动查看细节
图 8|处置从收回已经签发的身份开始,以代码、发布和生产验收共同防止复发。

研究记录

9证据、对象与来源

下面保留本文实际使用的标识、时间和原始材料,便于继续调查。

9.1研究对象

报告涉及的产品、行为者、技术、受影响对象和控制点。

CVECVE-2026-28514

Rocket.Chat 企业 ddp-streamer 密码认证绕过

安全公告GHSA-w6vw-mrgv-69vf

Rocket.Chat 项目安全公告

端点/websocket

DDP login 方法可达入口

组件ee/apps/account-service

企业微服务账户认证组件

修复提交dd7032a0a4c25b6be5b3af05c83f8d07d5102424

加入密码比较等待操作的上游提交

修复版本7.8.6 / 7.9.8 / 7.10.7 / 7.11.4 / 7.12.4 / 7.13.3 / 8.0.0

项目公告列出的各发行线修复下限

9.2事件时间

  1. 漏洞私下报告

    认证问题通过项目的私有漏洞报告渠道提交。

  2. 修复提交与 8.0.0

    上游加入等待操作,公开披露时间线记录 8.0.0 发布初始修复。

  3. 7.13.3 发布

    7.13.3 固定源码包含修复后的密码判断。

  4. 项目公告公开

    Rocket.Chat 发布 GHSA-w6vw-mrgv-69vf 并列出七条修复线。

  5. 技术分析公开

    GHSL-2026-004 与相邻问题的协调披露分析发布。

  6. SOSEC 完成源码复核

    SOSEC 核对公开公告、固定源码、修复提交、修复版本下限、检测条件与处置说明。

9.3来源与材料

  1. Rocket.Chat GHSA-w6vw-mrgv-69vf 项目安全公告https://github.com/RocketChat/Rocket.Chat/security/advisories/GHSA-w6vw-mrgv-69vf
  2. GitHub Security Lab Rocket.Chat 技术公告https://securitylab.github.com/advisories/GHSL-2026-004_GHSL-2026-005_Rocket_Chat/
  3. CVE Program 的 CVE-2026-28514 记录https://www.cve.org/CVERecord?id=CVE-2026-28514
  4. NVD 的版本、CVSS 与 SSVC 记录https://nvd.nist.gov/vuln/detail/CVE-2026-28514
  5. 加入等待操作的 Rocket.Chat 修复提交https://github.com/RocketChat/Rocket.Chat/commit/dd7032a0a4c25b6be5b3af05c83f8d07d5102424
  6. 受影响快照的密码验证函数https://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/lib/utils.ts
  7. 受影响快照的用户名登录函数https://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/lib/loginViaUsername.ts
  8. 受影响快照的账户登录分流https://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/Account.ts
  9. 受影响快照的会话持久化函数https://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/account-service/src/lib/saveSession.ts
  10. 受影响快照的 DDP login 入口与连接状态https://github.com/RocketChat/Rocket.Chat/blob/05c415b94cb91907de39a39c6d277579258f334e/ee/apps/ddp-streamer/src/configureServer.ts
  11. Rocket.Chat 7.13.3 修复后的用户名登录函数https://github.com/RocketChat/Rocket.Chat/blob/7.13.3/ee/apps/account-service/src/lib/loginViaUsername.ts
  12. Rocket.Chat 7.13.3 发布记录https://github.com/RocketChat/Rocket.Chat/releases/tag/7.13.3
  13. Rocket.Chat 8.0.0 发布记录https://github.com/RocketChat/Rocket.Chat/releases/tag/8.0.0
  14. Rocket.Chat 微服务架构与扩容文档https://docs.rocket.chat/docs/microservices
  15. Rocket.Chat 设备和其他登录位置管理文档https://docs.rocket.chat/docs/manage-your-account-settings
  16. ECMAScript ToBoolean 规范https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toboolean
  17. typescript-eslint no-misused-promises 规则https://typescript-eslint.io/rules/no-misused-promises/
  18. typescript-eslint no-floating-promises 规则https://typescript-eslint.io/rules/no-floating-promises/
  19. MITRE CWE-287 Improper Authenticationhttps://cwe.mitre.org/data/definitions/287.html