Skip to content

analytics: 让 executeAggregate 桥携带 ExecutionContext(#3597 的纵深防御第二层)+ 两处残留无 scope 调用 #3602

Description

@os-zhuang

#3597 / #3601 拆出的后续项。#3601 已从 analytics 层堵死泄露(ObjectQLStrategy 现在把 read scope AND 进 filter),本 issue 处理第二层以及同一接缝上的两处残留。

1. 主项:让 executeAggregate 契约携带 ExecutionContext

#3597 的根因之一是 analytics→engine 的桥不传 context:

// services/service-analytics/src/plugin.ts:197-208
const rows = await engine.aggregate(objectName, {
  where: filter, groupBy, aggregations, timezone,   // ← 无 context
});

engine 侧本来是支持的 —— BaseEngineOptionsSchema(spec/src/data/data-engine.zod.ts:56-59)带 context: ExecutionContextSchema.optional(),engine.ts:3339mergeReadContext。但 .optional() 意味着类型系统从不强迫调用方传,于是 analytics 桥就没传,进而命中 plugin-security 的无 principal fall-open(security-plugin.ts:775-781),把 engine 自己的 RLS 注入整个跳过。

为什么值得做(即便 #3601 已堵住泄露):

落点(需要动 packages/spec,会碰 api-surface 门,建议独立 PR):

  • spec/src/contracts/analytics-service.ts:241-252 —— executeAggregate options 加 context?: ExecutionContext
  • StrategyContext —— 透传 ExecutionContext(或至少把 context 一路带到桥)。
  • services/service-analytics/src/plugin.ts:197-208 —— 桥转发 context。
  • analytics-service.tscallCtx 已经拿到了 context,只是没往下带。

注意:这条不能替代 #3601。engine 侧 RLS 与 analytics 的 getReadScope 是两套解析(前者走中间件,后者走 security.getReadFilter),两者都保留才是纵深防御;而且没装 plugin-security 的部署只有 analytics 这一层。

2. 残留:fetchRecordLabels 走的是同一条无 scope 的桥

// services/service-analytics/src/plugin.ts:348-364
const rows = await executeAggregate(targetObject, {
  groupBy: ['id', displayField],          // ← 每条记录一行
  aggregations: [{ field: 'id', method: 'count', alias: '_c' }],
  filter: { id: { $in: ids } },           // ← 无 read scope、无 context
});

groupBy: ['id', displayField] 等于逐记录返回真实显示名 —— 粒度上是行级而非聚合级。

当前是否越权:否。 #3601 之后聚合本身已加 scope,喂进来的 ids 已被限制在调用者可见范围内。所以这是潜在债而非活漏洞:一旦 ids 的来源变宽(新调用方、新 strategy),它会立刻变成行级泄露,且没有任何一层会拦。

建议:要么让它也吃 read scope,要么在 #1 落地后靠 context 兜底。

3. 残留:ObjectQLStrategy.generateSql 预览不含 scope

objectql-strategy.ts:117-149 生成的展示性 SQL 完全没有 WHERE 子句,所以 /analytics/sql 的预览与实际执行的语义不符(实际执行在 #3601 后是加了 scope 的)。

不构成泄露(该 SQL 不执行,只用于展示),但会误导排查者。修的话需要把 FilterCondition 编成 SQL —— read-scope-sql.tscompileScopedFilterToSql 已有能力,只是 ObjectQL 的 generateSql 没有 NativeSQL 那套 alias 体系,需要小心。优先级最低。


🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions