Filing gate: ① a product defect with a named landing site and a reach:. Finding class (a). reach: is a public door: security.explain, served at REST POST /api/v1/security/explain and called by @objectstack/client. Measured through the security service on better-sqlite3, sqlite-wasm and PostgreSQL 16.
Filed by the domain:engine execution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN, os-warren) from the #20355 dev's out_of_scope_findings[0] (os-dev-report 5868561524 on #20355, PR #20427). The readings are the dev's. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens
Take a row-level policy whose using compares two fields that share no comparison class, such as record.status != record.amount (text and a number). #20347 classified this comparison: the platform defines no answer for it, and driver-sql refuses every read it scopes.
find for the caller answers INVALID_FILTER / 400.
security.explain for record r1, same caller, answers record: { visible: true, decidedBy: 'rls' }.
- With
record.amount > record.status (the same two columns, the other way round), explain answers visible: false against the same 400.
So explain gives a yes or a no, depending on how the raw values happen to compare, for a read that enforcement refuses outright. Explain exists to tell a builder or an admin what enforcement will do.
Where
packages/plugins/plugin-security/src/explain-engine.ts: the record attribution calls matchesFilterCondition(record, filter) (@objectstack/formula) with no declared columns, at origin/main 24b708593 (the call around :872). The evaluator has no schema, so it cannot know the two columns are of different classes, and it compares the raw values.
Fix shape, once PR #20427 lands (#20355, the write-check half of the same classification): matchesFilterCondition takes an optional third argument, options.fields (the object's declared columns). Given it, every cross-class { $field } comparison throws INVALID_FILTER / 400 before any record is read. Explain can hand it the columns the way PR #20427's write gate does, and then either answer what enforcement answers or refuse the same way. Which of those two explain should report is triage's call.
Related, not the same
Dedupe
search_issues in objectstack-ai/objectstack, open and closed:
- "security explain record attribution cross-field comparison class visible INVALID_FILTER": 12 hits.
- "explain-engine matchesFilterCondition declared fields security.explain visible true find refused": 16 hits.
The hits are #20355, #20347, #20002, #19986, #19963, #19995 and older disclosure and explain-layer cards. None is this.
Dedupe words: explain record attribution cross-field comparison class · security explain visible INVALID_FILTER · explain-engine matchesFilterCondition fields
Filing gate: ① a product defect with a named landing site and a
reach:. Finding class (a).reach:is a public door:security.explain, served at RESTPOST /api/v1/security/explainand called by@objectstack/client. Measured through the security service on better-sqlite3, sqlite-wasm and PostgreSQL 16.Filed by the
domain:engineexecution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN,os-warren) from the #20355 dev'sout_of_scope_findings[0](os-dev-report5868561524 on #20355, PR #20427). The readings are the dev's. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
Take a row-level policy whose
usingcompares two fields that share no comparison class, such asrecord.status != record.amount(text and a number). #20347 classified this comparison: the platform defines no answer for it, and driver-sql refuses every read it scopes.findfor the caller answersINVALID_FILTER/ 400.security.explainfor recordr1, same caller, answersrecord: { visible: true, decidedBy: 'rls' }.record.amount > record.status(the same two columns, the other way round), explain answersvisible: falseagainst the same 400.So explain gives a yes or a no, depending on how the raw values happen to compare, for a read that enforcement refuses outright. Explain exists to tell a builder or an admin what enforcement will do.
Where
packages/plugins/plugin-security/src/explain-engine.ts: the record attribution callsmatchesFilterCondition(record, filter)(@objectstack/formula) with no declared columns, atorigin/main24b708593(the call around :872). The evaluator has no schema, so it cannot know the two columns are of different classes, and it compares the raw values.Fix shape, once PR #20427 lands (#20355, the write-check half of the same classification):
matchesFilterConditiontakes an optional third argument,options.fields(the object's declared columns). Given it, every cross-class{ $field }comparison throwsINVALID_FILTER/ 400 before any record is read. Explain can hand it the columns the way PR #20427's write gate does, and then either answer what enforcement answers or refuse the same way. Which of those two explain should report is triage's call.Related, not the same
explain-enginecatches the rejection intonulland the record matcher readsnullas "no filter", while enforcement refuses the same read #20002 (closed): explain reported visible when the sharing read filter threw. That is the same "explain says visible, enforcement refuses" family, but through a caught rejection read as "no filter", not through a comparison the evaluator judges.readasks sharing without the caller's read depth —record.visibleis false on rows the caller'sfindreturns #19986 and plugin-security: the record-grained explain verdict forupdateis not computed with the write path's inputs —record.visibleis false on rows the by-id PATCH admits, so every consumer hides Edit from permitted users #19963 (closed): explain'sread/updateverdicts were computed from inputs other than the enforcement path's.Dedupe
search_issuesinobjectstack-ai/objectstack, open and closed:The hits are #20355, #20347, #20002, #19986, #19963, #19995 and older disclosure and explain-layer cards. None is this.
Dedupe words:
explain record attribution cross-field comparison class·security explain visible INVALID_FILTER·explain-engine matchesFilterCondition fields