漏洞
账号 ID 一直都在,这条筛选查询为何仍看见了别的租户
Chatwoot 2.2.0 至 4.11.2 之前版本受 CVE-2026-44706 影响,因为已认证筛选值与持久化自定义属性键在 FilterService 中取得了 PostgreSQL 语法身份;默认把全部 Web 与后台进程升级至当前官方版 4.16.1,清点遗留键名和导出任务,并在实际数据库角色下通过合成双账号验收,4.11.2 只作为历史首个完整修复下限。

文章导航
Chatwoot 在 2026 年 5 月 5 日发布 GHSA-9pgm-75gg-6948:2.2.0 起至 4.11.2 之前的版本存在高危、需要低权限认证的筛选 SQL 注入,4.11.2 是最低修复版本。公告同时说明没有可靠的长期绕过方案。截至 2026 年 7 月 26 日,项目的当前官方版本是 4.16.1。生产变更采用发布时重新核对的当前版本;4.11.2 留作暴露范围与首修版本的历史证据。升级后再用本地证据判断请求是否到达、查询是否执行以及哪些数据和凭据需要处置。
问题位于账号已经完成认证和选择之后。比较值在旧版 lt_gt_filter_values() 中从 Ruby 数据变成带引号的 SQL 片段;自定义属性键还能先进入存储,再在日后的 JSON 提取表达式中被插入 SQL。账号条件仍可能出现在查询文本里,注入表达式却能改变布尔结构,或按共享 PostgreSQL 角色的权限计算子查询。路由上的 account_id 因而无法替数据库解析器约束内层语法。
GitHub Security Lab 将分析样本固定在 v4.11.0,并追踪会话筛选、联系人筛选和联系人导出三条执行路径。公开材料建立了漏洞与源码影响,没有给出某个具名部署已遭利用的证据;本地事件等级必须由运行制品、已认证请求、后台任务、数据库会话和观测到的后果逐层建立。
1 处置单先把清单中的每个进程升至当前 4.16.1
修复目标包含所有能加载筛选服务的对象:Web 实例、联系人导出后台进程、定时或重试任务、控制台镜像、地区副本和下游分支。网页显示的新版本只能代表当前响应实例;一个仍可领取 Account::ContactsExportJob 的旧 worker 足以继续运行脆弱构造器。
1.2 六个词定位账号条件在整条查询中的权限边界
租户是 Chatwoot 中由账号标识的一组业务数据与权限。外层查询是从当前账号的会话或联系人关系起步的 ActiveRecord relation。内层表达式是筛选条件里由字段、操作符和值组成的 predicate;它能改变哪些外层行匹配,也能容纳子查询。前三个概念属于应用查询的结构。
共享数据库角色是 Web 或 worker 连接 PostgreSQL 时真正执行 SQL 的主体。bind 参数把数据和值的 SQL 模板分开,让数据库驱动传递类型化对象。RLS 是 PostgreSQL 行级安全;它只有在目标表启用、对实际角色生效并获得可靠租户上下文时,才会独立限制可见行。后三个概念决定数据库怎样解析和执行这条结构。
旧实现跨过了中间那道界线:请求值或存储键参与书写内层表达式。账号 ID 即使仍在外层文本里,新的 OR 结构可能扩大结果,标量子查询也能按共享角色读取别的对象。修复版把标量放回 bind,把键名限制为产品允许的词汇,并在最终构造位置按上下文处理。
2 三条执行路径和一份存储输入在同一构造器会合
2.1 同步筛选、后台导出和定义创建拥有不同的证据时钟
会话筛选在修复提交的 conversations_controller.rb 第 50–58 行把当前用户、当前账号和筛选参数交给 Conversations::FilterService。联系人筛选在 contacts_controller.rb 第 62–71 行进入 Contacts::FilterService。两条同步路径的 HTTP 请求、应用执行和 SQL 往往能落在同一条链路里。
联系人导出在同一控制器 第 45–49 行只负责把参数与身份放入 Account::ContactsExportJob;任务在 contacts_export_job.rb 第 27–35 行才调用筛选服务。HTTP 返回成功可以只代表入队,真正查询由另一个进程在稍后执行。调查必须连接 request ID、job ID、入队与重试、worker 制品、数据库会话和导出结果。
自定义属性定义在 custom_attribute_definitions_controller.rb 第 10–14 行进入账号存储。创建动作本身不执行脆弱查询;键名等到后续筛选引用时才进入 SQL。当前定义、变更历史、删除记录、已保存筛选和导入/恢复路径因此都属于排查对象。
2.2 比较值与自定义键名跨过了两处语法边界
第一处边界是日期与数字比较。旧 lt_gt_filter_values() 先读取 values[0],再把操作符、引号、外部值和 PostgreSQL 类型转换拼成一个字符串。类型转换写在 SQL 里无法保护解析阶段;PostgreSQL 必须先判断引号和表达式结构,之后才有机会执行日期或数值转换。最终的 where(query_string, filter_values) 即使带着其他 bind,也不会把已经写进 query_string 的标量恢复成数据。
第二处边界是 JSON 属性键。旧 build_custom_attr_query() 与 not_in_custom_attr_query() 把 @attribute_key 放入 ->> 操作数。键名可以早于筛选请求进入存储,后续任何引用它的筛选才触发构造。它与即时比较值需要两套历史连接,却在同一个数据库执行点产生结构含义。
修复为两类位置分配不同控制。日期和数字分别经过 Date.iso8601 与 BigDecimal,类型错误在 Ruby 层转成 CustomExceptions::CustomFilter::InvalidValue;合法对象存入 value_N bind,结构字符串只保留程序选定的操作符和占位符。自定义键名先按字母、数字、下划线、句点和连字符的产品词汇校验,再在正向、反向和 additional-attribute 构造点通过固定模板安全处理。
3 账号条件留在文本中,布尔结构和数据库权限仍能扩大数据范围
3.1 外层行范围与内层表达式要分开画
GitHub Security Lab 展示的公开调用链说明,注入值可以改变括号与 OR 的组合顺序,使原本围绕当前账号构造的条件不再承担完整限制。另一种盲式读取方式是在 predicate 中计算标量子查询,再通过真假、错误或耗时改变当前账号筛选的行为。两种结果都发生在账号路径已经通过之后。
因此源码复核要记录两个对象。外层 relation 回答正常查询从哪些会话或联系人起步,内层 expression 回答 PostgreSQL 实际计算了什么。看到 current_account、Current.account 或 account_id 只能建立第一层;直到所有动态值、键名、操作符和分组都完成构造,第二层才闭合。
这也解释了公告为何把影响范围记为“跨越权限边界”。已认证用户持有账号级应用权限,SQL 由连接角色计算。如果这个角色能读取多个账号的共享表、全局用户、访问令牌、会话、联系人或集成配置,表达式就继承相应上限。Chatwoot 公告据此要求对跨租户数据和凭据采取高优先级处置。
3.2 本地影响上限由事故时的 PostgreSQL 角色决定
每套部署都要从 Web 与 worker 实际使用的连接凭据建立可达图:数据库端点、current_user、角色成员关系、search_path、模式权限、表和视图、函数执行权、外部数据链接、所有者身份、RLS 启用与强制状态。迁移和管理任务使用的更强角色只有在普通请求进程能取得时才进入这条上限。
RLS 需要运行时证明。策略要位于相关表,对实际角色生效,并在连接池复用、事务回滚和超时之后继续获得正确租户上下文。表所有者、特权角色、带特殊执行上下文的函数和由其他主体拥有的视图都可能改变结果。系统目录快照加合成双账号测试可以直接记录当前行为。
可达图记录调查上限,实际读取数量由执行证据与后果证据另行确定。上限决定哪些数据集、凭据和租户需要检索与保守处置,后两类证据再决定是否提升事件等级。整改后的最小权限快照只描述当前状态;事故时的旧权限需要依赖初始快照,因此授权调整前先保存原始状态。
全局对象要单列。用户身份、安装设置和共享令牌可能没有 account_id,却影响多个账号;应用层加密只有在数据库角色无法取得解密能力时才会缩小上限。对每个可读对象记录租户键、敏感字段、密钥位置、凭据责任人和轮换影响,客户通知才能从“整个数据库”收敛到具体数据类别。
4 历史影响从资格、到达、执行和后果逐级建立
4.1 每一级只回答一个更具体的问题
- 资格:目标进程运行
2.2.0至4.11.2之前的制品。该状态要求进入排查范围。 - 路径到达:已认证身份调用三条筛选/导出路径之一,或创建了后续被引用的自定义定义。该状态建立入口和主体。
- 危险输入:请求值或存储键名违反修复版的类型、键名或结构约定。该状态把普通筛选与需升级检索的事件区分开。
- 执行:应用异常、数据库会话、归一化查询、计划或时延与同一请求/job 对齐。该状态说明输入到达了查询计算。
- 后果:查询结果、导出、信息信号、凭据使用或租户数据事件形成可解释连接。该状态支撑泄露、凭据处置和通知结论。
版本扫描一旦发现旧制品就触发修复;事件通报则引用阶梯上已经达到的最高一级。升级可以立即启动,跨租户泄露结论仍需执行或后果证据。
4.2 Web、队列、进程、数据库和定义历史必须落到同一事件
同步路径保存账号、不可变操作者或令牌 ID、请求 ID、路由、时间、状态、响应大小、应用实例、发布修订、数据库会话和查询标识。请求体最小化保存操作符、定义 ID、值数量、长度分组和带密钥摘要即可;原始客户字段进入受限证据库。
导出路径再加入 job ID、队列、入队/开始/完成时间、重试次数、领取它的 worker 摘要和输出文件处置。定义路线加入对象 ID、账号、创建者、键名摘要、模型、时间戳、更新/删除历史、已保存筛选引用。一个 HTTP 200 只覆盖自己所在的阶段;稍后执行的 worker SQL 需要独立执行记录。
数据库侧保留事故时实际配置:会话用户、应用名称、查询 ID、归一化结构、耗时、行、错误、取消、等待与日志时区。pg_stat_statements 会聚合和归一化,托管性能工具可能抽样;每个空结果都要带上容量、重置时间、留存、阈值和抽样规则。遥测缺口写明最早/最晚可靠时间、责任人、补偿证据和决定日期。
时间线至少保留脆弱制品首次上线、现存日志最早覆盖、修复制品在全部进程生效、遗留键名清理和可达凭据撤销五类时间。一个“已打补丁”时间戳无法回答后台旧进程、存量数据和身份后果分别在何时完成处置。
5 验收要证明值留在 bind、非法对象停在 SQL 之前
5.1 上游回归把修复意图固定在查询结构与 Ruby 类型上
联系人测试在 第 281–312 行断言标准日期使用 :value_0 且保存为 Date,非法形式抛出 InvalidValue;同文件 第 411–475 行覆盖自定义日期、BigDecimal 数值和拒绝路径。会话测试在 第 420–452 行对 created_at 执行同样的结构断言。
键名测试在 custom_attribute_definition_spec.rb 第 9–57 行接受下划线、连字符、句点和 Unicode 字母,拒绝引号、内部空格、分号和括号,并确认首尾空白被去除。修复后的 helper 在 第 48–60 行还把 additional-attribute 键送入固定模板的安全处理。
这些测试说明维护者修了哪条约定:结构由程序选择,标量先类型化再绑定,键名遵守产品词汇并在最终上下文处理。部署方还需补上真实控制器、队列、数据库角色、进程替换和历史数据,并用制品摘要与进程证据确认每个生产对象实际加载的代码。
5.2 合成双账号矩阵在不触碰生产数据的条件下关闭路径
隔离环境创建账号 A 与 B,每个账号放一条带独特合成值的会话和联系人,数据库角色使用与生产同等或更宽的只读可见性。先执行合法日期、数值、Unicode 键名和普通文本筛选,确认结果、导出和已保存筛选语义保持;再提交格式错误的日期/小数与新规则拒绝的键名,确认应用在匹配 SQL 发出之前产生受控错误。
矩阵覆盖会话筛选、联系人筛选和实际执行的联系人导出 job;自定义属性覆盖正向与 not_equal_to 分支;分组覆盖混合 AND/OR,并检查 bind 名称唯一。每个以 A 身份运行的结果和 CSV 都不能包含 B 的哨兵值,非法用例对应的数据库语句计数必须为零。
在启用 RLS 的环境,再用连接池复用、回滚和超时验证租户上下文被刷新。性能基线使用接近真实规模的合成数据比较修复前后的计划与耗时;出现兼容或规划问题时调整模板、索引或前向补丁,保持类型化 bind 与键名控制不变。
下游分支按行为验收。记录构建来源、镜像摘要和源码修订,把修复后上游版本的合法/非法矩阵原样运行在私有构建上;新增显示类型或操作符必须给出对应类型转换、bind 和模板测试。分支只有在三条路径、两类动态位置和异常处理均符合约定后,才能被视为等价修复。
6 结案需要修复进程、遗留数据和事件决定三类状态同时关闭
6.1 发布按保全、替换、清理、验证的顺序推进
先冻结主票据中的资产与证据,再用当前维护制品替换全部 Web 和 worker。滚动期间若新旧进程会同时接流量,相关路由保持临时限制;所有旧 worker 停止领取任务,重试和排队的导出要么由新进程接管,要么保留后重新创建。重启后以镜像摘要、修订号、启动时间和一次普通筛选证明实际对象已经变化。
异常自定义定义先保全,再由账号责任人决定迁移、隔离或停用。迁移同时更新 JSON 数据、已保存筛选和自动化引用,保留旧新对象映射与行数核对。备份恢复到业务环境时,在接收外部流量前重跑相同审计;离线证据副本保持原样。
凭据动作跟随第 3 章的可达图和第 4 章的事件等级。用于强可疑请求的令牌立即撤销;在有执行支持的事件中可读的 API 令牌、集成密钥、收件箱凭据或数据库凭据按依赖顺序轮换;密码哈希进入合理可达范围时,由产品、法务和身份责任人决定密码重置与会话撤销。每项处置记录纳入理由、使用方、切换时间和旧凭据最后使用时间。
6.2 回滚保留补偿控制,未知项带责任人结案
兼容缺陷优先前向修复。确需紧急回滚时,事件负责人批准的补偿控制必须先就位:精确路由限制、可信身份集合、语句超时、旧 worker 隔离、增强但最小化的遥测,以及自动到期。回滚只能落到仍受维护且包含完整修复的制品,不能选择仅达到 4.11.2 历史下限的旧构建;当前制品和数据迁移重新通过矩阵前,其他制品均保持禁止接流。
结案有四份相互独立的确认:软件清单显示全部执行对象已修复;数据清单显示定义与引用已合规、迁移、隔离或作为证据保留;数据库清单保存事故时授权并记录当前加固状态;事件清单为每个请求/job、可达凭据、数据集和租户给出处置。遥测缺口可以留下分析限制,同时必须有责任人、补偿动作、决定日期和剩余风险批准。
CVE-2026-44706 留下的工程结论很具体:账号路径约束谁能发起业务请求,查询构造器约束请求如何成为 SQL,数据库角色约束 SQL 能看见什么。三层在同一条链路中相互接续。4.11.2 标记类型化 bind 与键名规则首次完整进入发布版;生产应运行发布时仍受维护的当前版本,本次复核对应 4.16.1。资产、数据库权限和事件证据完成其余两层的运营闭环。
研究记录
7证据、对象与来源
下面保留本文实际使用的标识、时间和原始材料,便于继续调查。
7.1研究对象
报告涉及的产品、行为者、技术、受影响对象和控制点。
影响租户机密性的 Chatwoot 已认证筛选 SQL 注入
4.11.2 是历史最低修复版本;截至 2026 年 7 月 26 日,所有 Web 与后台进程默认升级至当前 4.16.1
需要认证的会话筛选入口
需要认证的联系人筛选入口
需要认证的联系人导出入口,随后由 Account::ContactsExportJob 执行
为后续筛选提供持久化自定义属性键名;创建本身不执行脆弱查询
加入类型化比较绑定、键名校验、安全处理与回归覆盖
7.2事件时间
- GitHub Security Lab 报告问题
GHSL-2026-059 通过 GitHub Private Vulnerability Reporting 提交。
- FilterService 修复落地
提交 432462f967 加入类型化绑定、自定义键名控制和回归测试。
- Chatwoot 发布 4.11.2
修复版本在提交后发布,包含已加固的 FilterService 对象。
- Chatwoot 发布安全公告
GHSA-9pgm-75gg-6948 记录受影响版本、认证前提、影响与修复版本。
- GitHub Security Lab 发布源码分析
GHSL-2026-059 在 v4.11.0 中追踪比较值进入脆弱 SQL 构造的过程。
7.3来源与材料
- Chatwoot GHSA-9pgm-75gg-6948 安全公告https://github.com/chatwoot/chatwoot/security/advisories/GHSA-9pgm-75gg-6948
- GitHub Security Lab GHSL-2026-059https://securitylab.github.com/advisories/GHSL-2026-059_Chatwoot/
- Chatwoot 修复提交 432462f967https://github.com/chatwoot/chatwoot/commit/432462f967373fe9a9a2722c374840115f9a6aee
- 父提交中脆弱的 lt_gt_filter_values() 第 84–98 行https://github.com/chatwoot/chatwoot/blob/a08125e283b8e15b84b5073def1e785137b059ba/app/services/filter_service.rb#L84-L98
- 父提交中自定义键名插值第 152–177 行https://github.com/chatwoot/chatwoot/blob/a08125e283b8e15b84b5073def1e785137b059ba/app/services/filter_service.rb#L152-L177
- 修复提交中类型化比较 bind 第 84–106 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/app/services/filter_service.rb#L84-L106
- 修复提交中转换、拒绝与键名构造第 160–212 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/app/services/filter_service.rb#L160-L212
- 修复提交中键名规则与归一化第 31–56 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/app/models/custom_attribute_definition.rb#L31-L56
- 修复提交中 additional-attribute 键名安全处理第 48–60 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/app/helpers/filters/filter_helper.rb#L48-L60
- 联系人标准日期 bind 与拒绝测试第 281–312 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/spec/services/contacts/filter_service_spec.rb#L281-L312
- 联系人自定义日期/数字 bind 与拒绝测试第 411–475 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/spec/services/contacts/filter_service_spec.rb#L411-L475
- 会话日期 bind 与拒绝测试第 420–452 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/spec/services/conversations/filter_service_spec.rb#L420-L452
- 自定义属性键名规则与归一化测试第 9–57 行https://github.com/chatwoot/chatwoot/blob/432462f967373fe9a9a2722c374840115f9a6aee/spec/models/custom_attribute_definition_spec.rb#L9-L57
- Chatwoot v4.11.2 发布页https://github.com/chatwoot/chatwoot/releases/tag/v4.11.2
- Chatwoot v4.16.1 当前版本发布页(截至 2026 年 7 月 26 日)https://github.com/chatwoot/chatwoot/releases/tag/v4.16.1
- Rails Active Record 查询条件与绑定https://guides.rubyonrails.org/active_record_querying.html#conditions
- Rails ActiveRecord 安全处理 APIhttps://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html
- PostgreSQL JSON 操作符与函数https://www.postgresql.org/docs/current/functions-json.html
- PostgreSQL 行级安全策略https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- PostgreSQL pg_stat_statementshttps://www.postgresql.org/docs/current/pgstatstatements.html