You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding: the #7929 read-scope disclosure is NOT analytics-only — an RLS $field filter injected by the security middleware discloses the same policy column on the ordinary CRUD path, and predates #7598 #7988
Observation-class finding, filed unassigned with no pm:queue — for triage to grade and route. Measured while executing the #7929 measurement dispatch (domain:engine-core seat, 2026-08-12). Recording, not claiming.
#7929's body, the triage block, and the 2026-08-12 maintainer ruling on it all frame the disclosure as a consequence of #7598's Q1 = B routing — "the refusal moved to the driver along with the compilation" — and the ruling's implementation boundaries scope the pins accordingly:
Pin both directions: (a) a read scope carrying a driver-refused reference answers the withheld envelope with no field-name echo — extend the analytics-query-read-scope-withhold pin family to this path
That framing is incomplete, and a fix scoped to it would leave the larger half open. The ordinary CRUD read path has the identical disclosure, and did not need #7598 to get it. It arrived with #5222 (2026-08-06), when driver-sql learned to compile { $field } and to refuse it with INVALID_FILTER / 400.
The two paths differ only in which component merges the admin's read filter into the query:
Both hand the driver a bare FilterCondition with no provenance mark, and driver-sql's applyCrossFieldComparison refuses identically in both cases.
Measured
Real ObjectQL engine, real SqliteWasmDriver seeded with the shared CROSS_FIELD_* corpus fixture, and a read-filter middleware shaped exactly like the security/sharing ones (ast.where = ast.where ? { $and: [ast.where, scoped] } : scoped) injecting an admin-authored policy predicate the caller never wrote:
code: INVALID_FILTER
status: 400
message: Operator "$gt" on field "amount" compares against another field
({ "$field": "secret_policy_column" }), which cannot be compiled here:
"secret_policy_column" is not a declared field of "cross_field_deal" —
only declared fields can be referenced. Cross-field comparison on SQL
push-down supports same-table columns the object declares, compared as
the same type class, excluding the tenant-isolation column.
names the admin-authored column: true
The 400 is a declared 4xx, so the REST boundary's withhold — declaresServerFault, which requires status >= 500 — can never reach it. That tiering is deliberate and already pinned (analytics-query-read-scope-withhold.test.ts, the "a DECLARED 4xx is untouched — the withhold is 5xx-only" case), so this is not a boundary bug; it is the same attribution seam #7929 records, one path over.
Scope of the exposure
Requires an administrator to have authored a CEL permission/RLS rule with a field-to-field comparison — compileCelToFilter emits { $field: path } for exactly that — whose reference fails one of the four #5222 rulings (dotted path, undeclared column, the tenant-isolation column on either side, or a cross-class comparison). Narrow, and no customer report; but on the CRUD path it reaches every SQL-driver read, not just the analytics face.
Observation-class finding, filed unassigned with no
pm:queue— for triage to grade and route. Measured while executing the #7929 measurement dispatch (domain:engine-coreseat, 2026-08-12). Recording, not claiming.Blocked-by: #7929
Why this is filed separately from #7929
#7929's body, the triage block, and the 2026-08-12 maintainer ruling on it all frame the disclosure as a consequence of #7598's Q1 = B routing — "the refusal moved to the driver along with the compilation" — and the ruling's implementation boundaries scope the pins accordingly:
That framing is incomplete, and a fix scoped to it would leave the larger half open. The ordinary CRUD read path has the identical disclosure, and did not need #7598 to get it. It arrived with #5222 (2026-08-06), when
driver-sqllearned to compile{ $field }and to refuse it withINVALID_FILTER/ 400.The two paths differ only in which component merges the admin's read filter into the query:
POST /analytics/queryObjectQLStrategy.withReadScopepackages/services/service-analytics/src/strategies/objectql-strategy.ts(withReadScope)opCtx.ast.wherepackages/plugins/plugin-security/src/security-plugin.ts:2198Both hand the driver a bare
FilterConditionwith no provenance mark, anddriver-sql'sapplyCrossFieldComparisonrefuses identically in both cases.Measured
Real
ObjectQLengine, realSqliteWasmDriverseeded with the sharedCROSS_FIELD_*corpus fixture, and a read-filter middleware shaped exactly like the security/sharing ones (ast.where = ast.where ? { $and: [ast.where, scoped] } : scoped) injecting an admin-authored policy predicate the caller never wrote:What the engine surfaces:
Producer chain from the captured stack, verbatim:
The 400 is a declared 4xx, so the REST boundary's withhold —
declaresServerFault, which requiresstatus >= 500— can never reach it. That tiering is deliberate and already pinned (analytics-query-read-scope-withhold.test.ts, the "a DECLARED 4xx is untouched — the withhold is 5xx-only" case), so this is not a boundary bug; it is the same attribution seam #7929 records, one path over.Scope of the exposure
Requires an administrator to have authored a CEL permission/RLS rule with a field-to-field comparison —
compileCelToFilteremits{ $field: path }for exactly that — whose reference fails one of the four #5222 rulings (dotted path, undeclared column, the tenant-isolation column on either side, or a cross-class comparison). Narrow, and no customer report; but on the CRUD path it reaches every SQL-driver read, not just the analytics face.Explicitly NOT claimed
service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598's doing. [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 Q1 = B is not implicated on this path at all — the CRUD read has gone straight todriver-sqlsince long before it. [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222's refusal arm is correct on its own terms; what is missing is the provenance to know the predicate was not the caller's.$field编译为列对列比较(cross-field comparison push-down) #5222, analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367, [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 Q1 = B and Q2 = A all stand. This records a scope fact about the remedy, not an objection to any of them.Refs
$field编译为列对列比较(cross-field comparison push-down) #5222 (the four cross-field rulings, and theINVALID_FILTER/ 400 refusal arm)service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 (the Q1 = B routing that brought the analytics face into the same seam)