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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions