漏洞影响与适用条件
SigNoz query-service 的链路漏斗分析入口缺少身份认证和权限检查。攻击者只要能通过网络访问受影响的 Web/API 服务,无需凭据或用户交互,就能发起分析查询。链路记录一次请求在系统中的执行过程,span 是其中的一项操作记录;漏斗用服务名和 span 名定义一组步骤,分析调用经过这些步骤时的数量、错误和耗时。
CVE 声明的受影响版本为 0.88.0 至 0.141.0,修复版本为 v0.141.1。运行证据覆盖 v0.140.0,v0.129.0 和 v0.141.0 另有源码层面的支持,整个版本区间并未逐一动态验证。存在缺陷的路由注册函数由社区版和企业版共用。
六个直接接收漏斗定义的接口不要求预先保存漏斗。未完成初始化向导、尚未创建用户的实例也已出现未认证访问。已初始化实例未单独重测,其同样受影响的判断来自路由注册逻辑,而非初始化状态的对照实验。查询要实际执行,还需要 ClickHouse 中的链路数据结构和目标表存在;这与入口是否进行身份检查是两个条件。
访问检查为何失效
正常情况下,分析请求应先经过 am.ViewAccess(),由访问控制中间件检查调用者身份及查看权限,再进入业务处理函数。RegisterTraceFunnelsRoutes() 在 pkg/query-service/app/http_handler.go 中注册了 12 条 POST 分析路由,却没有为它们添加这一检查。相邻的漏斗增删改查路由则使用了 am.ViewAccess() 或 am.EditAccess(),因此普通漏斗管理入口受到保护,并不能说明分析入口也受到保护。
其中六条路由直接接收完整的漏斗定义。对应的 *WithPayload 处理函数解码请求后,没有补做身份信息或请求所属组织的检查,就把时间范围和步骤交给查询模块。另六条 /api/v1/trace-funnels/{funnel_id}/analytics/* 路由按已保存的漏斗 ID 查询,也缺少同一层中间件角色检查;这一点有源码和补丁支持,但这些路由未被单独运行验证。
六条直接接收定义的路由均以 /api/v1/trace-funnels/analytics/ 开头,使用 POST 和 JSON 请求体:
| 路径后缀 | 查询用途或已知行为 |
|---|---|
validate |
校验漏斗定义对应的链路;处理函数要求至少两个步骤 |
overview |
返回漏斗的时间序列分析 |
steps |
返回各步骤的 span 数量和错误计数 |
steps/overview |
返回步骤的时间序列分析 |
slow-traces |
返回慢链路标识 |
error-traces |
返回错误链路标识 |
攻击者通过 start_time 和 end_time 选择时间窗口,取值为以 Unix 纪元为起点的纳秒时间戳;通过 steps 中的 service_name 和 span_name 选择步骤。steps/overview、slow-traces 和 error-traces 还使用 step_start、step_end。除 validate 外,其余五个处理函数没有执行同样的“至少两个步骤”检查。
查询构造器把这些输入转换为 SQL,再通过 GetListResultV3 在 ClickHouse 中执行,涉及 signoz_traces.distributed_signoz_index_v3 等链路表。关键边界在查询之前就已失守:调用者尚未通过身份和权限检查,服务端已经允许其选择分析对象并调用数据库。
这种能力可能暴露链路标识、耗时、span 计数、服务调用关系、错误活动及时间序列统计。已观察到的 v0.140.0 数据集没有链路记录,各步骤计数为零,慢链路和错误链路结果为 null。因此,有数据时的泄露范围来自接口功能和查询语义,不能表述为已经读取了包含真实链路的完整数据集。
附加链路:SQL 注入如何变成响应内容泄露
v0.140.0 还存在 SQL 注入和服务端请求伪造(SSRF)的运行证据。SQL 注入是指输入被当作 SQL 语法执行;SSRF 则是利用服务端向攻击者指定的地址发起请求。这两项是附加发现,不属于本 CVE 所描述的访问控制缺失,其受影响版本范围不能直接沿用本 CVE 的范围。
查询构造器使用 fmt.Sprintf,将步骤的 ServiceName 和 SpanName 未经转义地填入 ('%s','%s') AS step%d。这一模板在 WITH 子句中定义供后续查询使用的二元组,也就是一对服务名和 span 名。本应只作为字符串数据的输入,因直接拼接而有机会改变 SQL 结构。已观察到的方式利用两个步骤中的 SQL 分隔符及配对块注释,重组合法元组,并使 SELECT 计算攻击者控制的表达式。
结果泄露依赖另一条通道:数据库错误会经 RespondError 原样进入 HTTP 500 响应。攻击者让表达式产生字符串,再迫使数据库将它转换为 UInt8,即 8 位无符号整数。无法转换的字符串会被写进 ClickHouse 错误信息,随后返回给调用者。这样,即使正常响应不包含选中的内容,错误响应仍可能暴露表达式结果、查询文本或数据。
ClickHouse 的 url() 表函数能够通过 HTTP 获取内容,并将其作为查询数据。把它放进被注入的表达式,再让获取的文本进入上述转换错误通道,就能让 ClickHouse 发起请求并回传响应内容。已观察到该链路取回 SigNoz 服务 /api/v1/version 的 JSON 响应。可访问的目标由 ClickHouse 服务器的网络环境决定,整条链还依赖数据库账号权限以及 url() 的可用性。
已观察到的注入在单条 SELECT 内生效,多语句被拒绝;目标端代码执行、文件写入或数据库写入均未得到证明。ClickHouse 25.12.5 上观察到的写入限制依赖具体版本和配置,不能据此认定所有部署都不存在可写路径。
修复与核查
受影响部署应升级到 v0.141.1 或更高版本。提交 f78bd49(PR #12817)为全部 12 条链路漏斗分析路由补上 am.ViewAccess(),将访问检查恢复到处理函数之前。补丁预期会阻止等价的未认证请求继续进入查询流程;这一预期依据代码变更,修复后的拒绝行为尚未经过动态验证。
这一修复确认覆盖的是缺失的访问检查。现有修复信息没有确立不安全 SQL 拼接、数据库错误泄露或 url() 行为的独立修复,不能把升级后的认证要求等同于附加问题已全部消除。
核查时,应区分版本、请求方法与请求实际到达的位置:
- 公开的
/api/v1/version返回版本值,可用于与受影响范围比对。分析接口只接受 POST;对同一路径发送普通 GET,可能得到状态码为 200 的单页应用 HTML,这不证明分析数据已暴露。 - 未认证 GET
/api/v1/trace-funnels/list返回 401,而未认证 POST 分析请求返回查询结果或查询执行后的错误,是访问检查不一致的具体信号。已有六个直接接收定义的入口到达处理函数的运行证据。 - HTTP 500 本身不能说明请求被认证机制拦截。
overview和steps/overview曾在查询后因NaN(非数值)无法序列化到 JSON 而失败;这类失败发生在请求已经越过访问边界之后。 - 响应中的
error converting clickhouse results to list,以及包含字符串内容的Cannot parse string ... as UInt8,可作为排查转换错误泄露的线索。应结合具体请求输入和错误内容判断;单独出现这些文本不足以认定发生了注入或 SSRF。