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
A bare-triple filter whose operator is not in VALID_AST_OPERATORS is passed through unconverted and then skipped entirely by the SQL driver. No WHERE clause is emitted, no error is raised, and the caller gets every row. The user sees a list they believe is filtered and it isn't.
This is reachable today through an ordinary ObjectUI view-config affordance — it is not hypothetical.
The chain
Each hop verified by reading the code; the spec hop was executed.
1.isFilterAST gates a bare triple on the operator being known — packages/spec/src/data/filter.zod.ts:400:
3.applyFilters walks that array — packages/plugins/driver-sql/src/sql-driver.ts:3702-3708. For a bare triple the three elements are all strings, so each hits the string branch, matches neither and nor or, and is continued:
for(constitemoffilters){if(typeofitem==='string'){if(item.toLowerCase()==='or')nextJoin='or';elseif(item.toLowerCase()==='and')nextJoin='and';continue;// <-- 'close_date', 'before', '2024-01-01' all land here}if(Array.isArray(item)){ ... }}
No Array.isArray branch is ever taken. No predicate is emitted.
driver-memory has the same shape (default: return null, memory-driver.ts:768).
Executed evidence
Against published @objectstack/spec:
["close_date","before","2024-01-01"] isFilterAST=false *** PASSED THROUGH UNCONVERTED ***
["close_date","after","2024-01-01"] isFilterAST=false *** PASSED THROUGH UNCONVERTED ***
["stage","not in",["won","lost"]] isFilterAST=false *** PASSED THROUGH UNCONVERTED ***
["status","=","active"] isFilterAST=true converted -> {"status":"active"}
Why those three operators, and why it is reachable
VALID_AST_OPERATORS (filter.zod.ts:352) contains not_in and nin but not'not in' (with a space), and contains neither before nor after.
ObjectUI's view-config builder emits exactly those three verbatim — packages/plugin-view/src/config/view-config-utils.ts:46:
'notIn': 'not in',// space — VALID_AST_OPERATORS has not_in / nin'before': 'before',// absent'after': 'after',// absent
and data-objectstack's normalizeFilterOperator ends ?? op, so they reach the wire unchanged.
The trigger is the single-condition case.toSpecFilter (view-config-utils.ts:158) returns a bare triple when there is exactly one condition with AND logic:
So: an author adds one date filter "before " to a view → the saved view returns every row, silently. Add a second condition and the shape becomes nested, which reaches the driver's wider switch and throws instead — the failure mode flips on how many conditions the user happened to add.
Scope of impact
Not a permission bypass, to be explicit about it. Row-scoping middleware $and-composes onto the same ast.where, so a scoped object yields {$and: [<raw array>, <scope>]}; the $and arm (sql-driver.ts:3987) calls applyFilterCondition on each element, and Object.entries(['close_date','before','2024-01-01']) produces keys "0"/"1"/"2" — it would attempt to filter on columns named 0, 1, 2 and error. The scope arm survives. (This arm is reasoned from the code, not executed.)
So the impact is: on objects without row-scoping, a silently unfiltered result set; on scoped objects, a confusing SQL error. Both are wrong; neither crosses a permission boundary.
Backends disagree on the failure mode
Path
Unknown operator →
driver-sql, $-object
throw (sql-driver.ts:4016)
driver-sql, nested AST
throw (sql-driver.ts:3866)
driver-sql, bare triple
silently drops the filter
driver-memory, AST
silently drops the condition (memory-driver.ts:768)
driver-mongodb, AST
passes $${op} raw to Mongo → server error
mapAnalyticsOperator
silently becomes $eq (protocol.ts:3800)
matches-filter (RLS)
false — fail closed
analytics preview-evaluator
true — fail open (preview-evaluator.ts:44)
Six behaviours for one input class.
Suggested fix
In applyFilters (sql-driver.ts:3702) and memory-driver.ts:768: an array item that is neither a recognised join keyword nor an array — and a bare triple whose operator is unrecognised — should throw, matching what the nested and $-object paths already do. Silently emitting no predicate is the one outcome that must not happen.
Consider making protocol.ts:2754 reject rather than pass through: if the value looks like a filter array but isFilterAST says no, that is a malformed filter, not an opaque where.
Independently, before / after / 'not in' are ObjectUI-side spellings that no spec vocabulary defines — tracked at Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned objectui#2945. They should be lowered client-side to < / > / not_in (ObjectUI's own ListView.tsx:75 already does exactly this for before/after; the view-config persistence path does not). Fixing that removes today's trigger but not the underlying silent-drop.
Why this blocks other work
Any narrowing of VALID_AST_OPERATORS — e.g. consolidating the filter-operator vocabularies — converts working bare-triple filters into silently unfiltered queries. Item 1 above should land before any such narrowing is attempted, otherwise the change is untestable: it passes CI, passes os build, and degrades quietly against stored view metadata.
A bare-triple filter whose operator is not in
VALID_AST_OPERATORSis passed through unconverted and then skipped entirely by the SQL driver. No WHERE clause is emitted, no error is raised, and the caller gets every row. The user sees a list they believe is filtered and it isn't.This is reachable today through an ordinary ObjectUI view-config affordance — it is not hypothetical.
The chain
Each hop verified by reading the code; the spec hop was executed.
1.
isFilterASTgates a bare triple on the operator being known —packages/spec/src/data/filter.zod.ts:400:2. The protocol treats that as a conversion gate, not a validation gate —
packages/metadata-protocol/src/protocol.ts:2754-2757:3.
applyFilterswalks that array —packages/plugins/driver-sql/src/sql-driver.ts:3702-3708. For a bare triple the three elements are all strings, so each hits the string branch, matches neitherandnoror, and iscontinued:No
Array.isArraybranch is ever taken. No predicate is emitted.driver-memoryhas the same shape (default: return null,memory-driver.ts:768).Executed evidence
Against published
@objectstack/spec:Why those three operators, and why it is reachable
VALID_AST_OPERATORS(filter.zod.ts:352) containsnot_inandninbut not'not in'(with a space), and contains neitherbeforenorafter.ObjectUI's view-config builder emits exactly those three verbatim —
packages/plugin-view/src/config/view-config-utils.ts:46:and
data-objectstack'snormalizeFilterOperatorends?? op, so they reach the wire unchanged.The trigger is the single-condition case.
toSpecFilter(view-config-utils.ts:158) returns a bare triple when there is exactly one condition with AND logic:So: an author adds one date filter "before " to a view → the saved view returns every row, silently. Add a second condition and the shape becomes nested, which reaches the driver's wider switch and throws instead — the failure mode flips on how many conditions the user happened to add.
Scope of impact
Not a permission bypass, to be explicit about it. Row-scoping middleware
$and-composes onto the sameast.where, so a scoped object yields{$and: [<raw array>, <scope>]}; the$andarm (sql-driver.ts:3987) callsapplyFilterConditionon each element, andObject.entries(['close_date','before','2024-01-01'])produces keys"0"/"1"/"2"— it would attempt to filter on columns named0,1,2and error. The scope arm survives. (This arm is reasoned from the code, not executed.)So the impact is: on objects without row-scoping, a silently unfiltered result set; on scoped objects, a confusing SQL error. Both are wrong; neither crosses a permission boundary.
Backends disagree on the failure mode
$-objectsql-driver.ts:4016)sql-driver.ts:3866)memory-driver.ts:768)$${op}raw to Mongo → server errormapAnalyticsOperator$eq(protocol.ts:3800)matches-filter(RLS)false— fail closedpreview-evaluatortrue— fail open (preview-evaluator.ts:44)Six behaviours for one input class.
Suggested fix
applyFilters(sql-driver.ts:3702) andmemory-driver.ts:768: an array item that is neither a recognised join keyword nor an array — and a bare triple whose operator is unrecognised — should throw, matching what the nested and$-object paths already do. Silently emitting no predicate is the one outcome that must not happen.protocol.ts:2754reject rather than pass through: if the value looks like a filter array butisFilterASTsays no, that is a malformed filter, not an opaquewhere.before/after/'not in'are ObjectUI-side spellings that no spec vocabulary defines — tracked at Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned objectui#2945. They should be lowered client-side to</>/not_in(ObjectUI's ownListView.tsx:75already does exactly this forbefore/after; the view-config persistence path does not). Fixing that removes today's trigger but not the underlying silent-drop.Why this blocks other work
Any narrowing of
VALID_AST_OPERATORS— e.g. consolidating the filter-operator vocabularies — converts working bare-triple filters into silently unfiltered queries. Item 1 above should land before any such narrowing is attempted, otherwise the change is untestable: it passes CI, passesos build, and degrades quietly against stored view metadata.Found while auditing spec-enum ↔ renderer coverage for objectstack-ai/objectui#2901.