Skip to content

/analytics/query 仍把 RLS 策略字段名回显给调用方 —— read-scope 拒收的泄漏在姐妹面上没堵,#5367 只堵了 dataset 路由 #5811

Description

@os-zhuang

现状

#5367(维护者 2026-08-06 裁决,方案 B)把 read-scope-sql.ts 的十处 fail-closed 拒收改成 READ_SCOPE_COMPILE_FAILED / 500,并让 POST /analytics/dataset/query withhold 任何声明了服务端故障(status >= 500 且带 code)的生产方 message。理由之一是这些 message 点名 RLS 策略的字段名与比较值:

[read-scope-sql] unsafe field identifier "secret_policy_field" — refusing to build read scope (fail-closed).
[read-scope-sql] unsupported operator "$regex" on "owner_email" (fail-closed).
[read-scope-sql] "approved_by_manager" has a nested/relation value which is not supported in a read scope (fail-closed).

这些内容来自管理员写的 sharing rule / permission set,由 security 服务编译后经 getReadScope 交给 analytics —— 调用方(租户)从来没写过它们,也不该能从错误体里读出来

同一批拒收在姐妹面上没有被堵。 /analytics/querydispatcher-plugin.errorResponseBase(packages/runtime/src/dispatcher-plugin.ts:445)退出,它的 message 判据是:

const message =
    httpStatus >= 500 && looksLikeInternalErrorLeak(raw)
        ? INTERNAL_ERROR_MESSAGE
        : raw || 'Internal Server Error';

looksLikeInternalErrorLeak(packages/types/src/error-leak.ts:47)是对 SQL/驱动措辞 的启发式(sqlite_SQLSTATE、以 SELECT/INSERT INTO/UPDATE/DELETE FROM 开头、constraint failed/unique constraint/foreign key)。实测把上面十一种 message 形态逐条喂进去:

ECHOED   [read-scope-sql] unsafe field identifier "owner id" — refusing to buil…
ECHOED   [read-scope-sql] unsafe alias identifier "crm opportunity" — refusing …
ECHOED   [read-scope-sql] read scope must be a filter object (fail-closed).
ECHOED   [read-scope-sql] "$and" requires an array (fail-closed).
ECHOED   [read-scope-sql] unsupported top-level operator "$nor" (fail-closed).
ECHOED   [read-scope-sql] bare array value for "region" — use { $in: [...] } (f…
ECHOED   [read-scope-sql] "owner" has a nested/relation value which is not supp…
ECHOED   [read-scope-sql] $in for "region" needs an array (fail-closed).
ECHOED   [read-scope-sql] $nin for "region" needs an array (fail-closed).
ECHOED   [read-scope-sql] $between for "amount" needs [min,max] (fail-closed).
ECHOED   [read-scope-sql] unsupported operator "$regex" on "owner" (fail-closed…

11/11 全部 ECHOED —— 即 errorResponseBase 会把策略内容原样写进 error.message,状态码是 500(err.status 被采纳)但正文没有被收口。

read-scope-sql.tscompileScopedFilterToSqlNativeSQLStrategy.applyReadScopeObjectQLStrategy 的回显 SQL 两条路上都会跑,而这两条路都服务 /analytics/query,所以这条面是真实可达的,不是理论问题。

为什么没在 #5367 里一起做

#5367 的题目是 dataset 路由那串 message 正则名单的退休。这条泄漏在本单之前就存在、且在另一条路由上,把它一起改会把一次可见行为变更(该面 message 从可读变为收口)夹带进一个以「退休正则名单」为题的 PR。按 PD #10 单独立项。

PR #5808(#5367 的第二个 PR)里的规则是就地写在 rest-server.ts ③ 分支内的三行判断,故意没有抽成共享谓词 —— 只有一个消费者时提升就是推测性面。本单是第二个消费者出现的时刻。

建议方向(不预判)

无论选哪个都要注意不能把 withhold 放宽到「所有 5xx」:#5667 刻意保留了未声明 5xx 的可读性(自产故障 "no strategy can handle query …" 应当照旧可读),PR #5808 的用例两侧都钉了这一点,新实现要沿用。

验收建议

  1. 规则落地后,/analytics/query 面加端到端用例:read-scope 拒收 → 500,error.messageInternal server error,body 不含策略字段名,error.details.codeREAD_SCOPE_COMPILE_FAILED(errorResponseBase 已经会把 err.code 放进 details,The dispatcher puts the HTTP status in error.code and parks the real code in details — pinned in #3687, still unfixed #3842),完整原文在日志/errorReporter 侧。
  2. 反向:未声明 5xx 在该面仍然可读(fix(service-analytics,rest): analytics dimension 的源字段闸门 —— 不存在的 dimension 答 400 INVALID_FIELD,dataset 500 不再回显 SQL (#5520) #5667 分级不被吃掉)。
  3. changeset 写清这是可见行为变更(该面 message 由可读变收口),照 analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352 changeset 里 "Observable behaviour change" 那一段的写法。

参考

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions