Repository navigation
security(analytics): the native-SQL strategy answers a query naming a field the caller has no field-level read permission for, where the engine and the ObjectQL strategy refuse 403 #20917
Description
Filing gate: ① a product defect with a measured reach. Class a. Security family, and it takes the "could leak data" exception. Filed by the domain:services seat (#6021, session session_01XY5uCwTjZj7884yYtyur4H) from #20887's dev report (5916988260, finding 1, redacted by this seat under the disclosure discipline).
What was measured
Measured by #20887's dev through the analytics routes on a SQL deployment, with the real SecurityPlugin, ObjectQL and SqlDriver, as a member whose field-level permissions hide one field. The probe was never committed.
- On the native-SQL strategy: a query that names that field is answered. This covers the positions the strategy compiles into SQL: the members it groups by, the members it filters on, and a member joined through a relationship. The answer carries the hidden field's values, or rows selected by them.
- For the same queries,
engine.findand the analytics ObjectQL strategy answer403 PERMISSION_DENIED. - Mechanism, read from the code:
NativeSQLStrategy(packages/services/service-analytics/src/strategies/native-sql-strategy.ts) compiles those members straight into SQL and holds no field permissions to check them against. The analytics layer's admission is object-level only.
This is the field-level counterpart of #4467 (record-level scoping on the same route, closed). #20887's PR #20916 closes it for the nested-relation form alone, where the native strategy now declines and the engine answers as the caller. It does not touch the other positions.
Not measured, for triage's first grade
- Other drivers' native paths: only SQLite was measured. The native strategy's gap is driver-independent as read, but that was not measured.
- The SQL echo (
/analytics/sql): whether it prints a hidden member was not measured. - Authored cubes versus inferred cubes: whether a cube's own declarations change the answer was not measured.
Direction (proposed, triage's to set)
One permission rule, the engine's: the analytics faces answer a field the caller may not read the way engine.find does. ⛔ No second copy of the rule in the analytics layer. Two possible directions:
- the native strategy declines a query that names a field the caller may not read, so the engine path answers it (the mechanism of the cross-field decline and of PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916's decline);
- or the admission step judges every named member against the caller's field permissions before either strategy runs.
Pins: a hidden field as a grouped member, as a filtered member and as a joined member, each on both strategies, answer the engine's refusal. A readable field is the control.
Reader who acts
Triage's first grade: it sets the grade and the lane. The positions sit in packages/services/service-analytics (domain:services, area:reports/area:access).
Dedupe (queries run before filing, closed included)
mcp__github__search_issues, repo-scoped, in the act that filed this card:
- 「analytics native SQL strategy field-level security unreadable field permission」: 7 hits.
- analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 and security(analytics): ObjectQLStrategy 不消费 getReadScope — NativeSQL 回落后聚合查询无 RLS/租户谓词(#2852 修复未覆盖的另一半) #3597 (closed) are record-level scoping on the analytics faces.
- security: the analytics ObjectQL execute face answers a row-level read scope it cannot run with
INVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 and finding: the #7929 read-scope disclosure is NOT analytics-only — an RLS$fieldfilter injected by the security middleware discloses the same policy column on the ordinary CRUD path, and predates #7598 #7988 (closed) are read-scope disclosures in refusal messages. - [security] A by-id write is not gated by record visibility — a contributor mutates records they cannot read, when only select-scope RLS is authored #7665, 元数据面未做字段级权限掩码:/meta 返回完整 fields,导入模板列不受 FLS 约束,客户端 field.permissions 守卫是死代码 #3661 and [permissions] 行级读可见范围无法按业务字段收窄:viewAllRecords 全有/全无两档之间缺共享规则 #4376 are other surfaces.
- None is field-level permissions on the analytics native path.
- 「analytics raw SQL leaks field values the caller cannot read, group by hidden field, filter oracle」: 9 hits, all closed. None is field-level permissions.
- 「analytics FLS field permissions dimension measure cube native path」: 13 hits, all closed ([finding] analytics: a cube / dataset dimension on a
jsonfield, compiled by NativeSQLStrategy, answers one group per serialized document on SQLite and 500 on PostgreSQL; the engine door #20783 closes does not see it #20807, [17.0-rc2验收] analytics: 不存在的 dimension 500(泄漏 SQL / SQLITE_ERROR)而不是 400 指名字段 —— #4437 只给 measure 加了闸门,dimension 侧对称缺口仍在 #5520, analytics:where里点名不存在的字段仍然一路到驱动 —— #4437(measure)/ #5520(dimension)之后,filter 面是同一个缺陷剩下的第三个 param #5669, analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 and others are other defects). None covers it.
Dedupe words: analytics native SQL field-level permission · NativeSQLStrategy FLS · analytics hidden field answered
Generated by Claude Code
Activity