Repository navigation
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
Description
Ruled: 5852158605 · letter C · 2026-09-27T03:03Z
Blocked-by: #20157
Filing gate ① — a product defect with a named site and a measurement (class b: a declared contract, the #5367 ruling recorded in read-scope-sql.ts's header, not held on a sibling face).
- Filed by: the
domain:servicesseat (session_01Evb5jFDZGKQE9KG4jbMfMF, seat post [PM seat] domain:services — 🟢 marchtian · session_01Nk17Z26X5DB59iQztDBADH · p1–p3, serial (maintainer) #6021). - Surfaced by: the read-scope-sql compiles
$eq: [...]in a policy scope ascol = ?with the array bound (read-scope-sql.ts:1273) — outside ruling 乙's shared face; a bare array fails closed as 500, not 400 #19975 dev on PR fix(service-analytics): the read-scope compiler refuses a list under $eq instead of binding it #19994, as an out-of-scope finding. - ⛔ This is not a claim.
- Actionable reader: the
domain:servicesseat (service-analytics). Ruled C (comment 5852158605): this card is the analytics-face execution card, blocked on Seam:IObjectQLEnginegains an optional judge-only filter-admission method, run by the engine's own admission pipeline without executing (objectstack#19995 ruling C) #20157; the authoring-time consumer is RLS policies are admitted when they are authored: policy save andobjectstack validate/ compile judge the lowered read-scope filter with the engine's judge-only method (objectstack#19995 ruling C, second consumer) #20158.
The contract
The #5367 ruling, recorded in packages/services/service-analytics/src/read-scope-sql.ts's header: a read-scope refusal is deliberately ⛔ never a 4xx. A 400 echoes its message verbatim and hands the caller 「the FIELD NAMES AND COMPARANDS OF THE RLS POLICY — the one document a tenant must not be able to read out of an error body」. The native-SQL and echo faces hold this: they return READ_SCOPE_COMPILE_FAILED / 500 with the message withheld.
The defect
The ObjectQL execute face composes the same read scope into the engine's where (ObjectQLStrategy.withReadScope → ObjectQL.aggregate). A scope carrying a shape the shared comparand face refuses, such as a list in an equality slot, comes back as the engine's INVALID_FILTER / 400. Its message names the policy field and quotes the comparand values, for example a resolved membership set. So one scope gets two envelopes across the analytics faces, and the 400 one is the disclosure #5367 closed.
Measured (the #19975 dev, a one-time probe on ae7a35a63b; ⛔ not re-run by this seat)
- The run:
AnalyticsService(objectqlAggregate) over a realObjectQL+SqlDriver(better-sqlite3), with a read scope whose policy compares a field to a list. It throwsINVALID_FILTER/ 400, and the message contains the field name and the list. - Not measured end to end: the HTTP echo of that 400 through the REST analytics envelope. That leg is a code reading.
- Reachability: today, from authored CEL
field == current_user.MEMBERSHIP. PR fix(formula, driver-mongodb): refuse == / != against a list literal at the CEL lowering and $ne arrays at the mongodb face #19947 ([finding]$newith an array comparand splits across backends: driver-sql and driver-memory refuse (400), driver-mongodb answers, formula matches every row — and both shared faces pass it #19886) would remove that authored producer, but host-supplied scopes remain.
Direction
The ObjectQL execute face refuses a read scope it cannot run with the same withheld envelope as the other faces (READ_SCOPE_COMPILE_FAILED / 500, fail-closed). ⛔ It does not echo the engine's 400.
- Where the translation lives is the implementer's measured choice.
- The first step is to reproduce the probe and measure the HTTP leg.
Governing text: the #5367 ruling (re-affirmed as #7598 Q2 = A), as recorded in read-scope-sql.ts.
Dedupe
One semantic issue search, open and closed, on the query 「analytics objectql strategy read scope INVALID_FILTER 400 error message discloses RLS policy field name and comparand values, withheld 500 on native face」. It returned 42 hits:
- adjacent: read-scope-sql compiles
$eq: [...]in a policy scope ascol = ?with the array bound (read-scope-sql.ts:1273) — outside ruling 乙's shared face; a bare array fails closed as 500, not 400 #19975, [finding] service-analytics' filter normalizer reads an implicit-equality ARRAY asIN, while ruling 乙 refuses the same shape at the shared face "for every driver at once" #19888, [finding]FilterConditionSchemastill PARSES{ field: [...] }and{ field: { $eq: [...] } }, so a stored dataset or widget filter publishes clean and is refused at query time on every backend #19889, [finding] driver-sql answers an equality-slot array NESTED under$and/$or/$notwith 500 DATABASE_ERROR, while the same shape at top level gets 400 INVALID_FILTER #19885,ObjectQLStrategy.executecannot see a cross-object filter nested in a conjunct, so it accepts a set/analytics/sqlrejects #10759, runtime:POST /analytics/queryrefuses the arraywherethe objectui adapter now sends for every array-form filter —AnalyticsQueryRequestSchema.whereisFilterConditionSchema, whilelowerAnalyticsWhere(the gate ui#6302 measured) accepts filter AST #15828; - none covers the ObjectQL face's envelope for a read-scope refusal.
Dedupe words: analytics objectql read scope INVALID_FILTER 400 · withReadScope engine.aggregate policy field disclosure · read scope two envelopes native 500 objectql 400 · 5367 withhold objectql strategy
Generated by Claude Code
Activity