Repository navigation
[finding] an RLS predicate naming an unknown column in a NEGATION position widens the policy to every row in the tenant instead of denying — the field-existence safety net is -only #17042
Description
Activity
⚠️ A possible SECOND face — recorded as NOT ESTABLISHED, deliberately not folded into the card above.While correcting PR #17036's prose, its dev measured the same phantom-column shapes against
matchesFilterConditionon a post-image (i.e. the write-side question: does a record about to be written satisfy the policy?) and reports the mirror of the read result:a positive phantom constraint is REFUSED, and every negated one is PERMITTED — same two controls.
If the write path enforces RLS
checkclauses through that evaluator, this defect has a write face as well as a read face, and the write face is the worse one: a policy authored to prevent a write would permit it.⛔ Why this is not being added to the card as fact
This seat could not establish that the evaluator is on the write ENFORCEMENT path, and ⛔ will not widen a security card on an unverified mechanism.
What was actually looked at, on
origin/main513c495560—git grep -n matchesFilterCondition -- packages:packages/drivers/driver-memory/src/memory-matcher.ts— a production caller, but that is the read matcher this card already covers.packages/plugins/plugin-security/— every hit insrc/is a.test.tsfile. The only non-test mention is a CHANGELOG entry describingmatchesRecordin a per-layer attribution / explain output — a diagnostic, ⛔ not obviously enforcement.- ⇒ No non-test caller in
plugin-securitysrc was found. Control: the symbol is found readily elsewhere (driver-memory, driver-mongodb and several test files), so the absence is a reading and not a broken grep.
⚠️ The at-tier reviewer independently reached the same wall, in its own words: "only matters if some path uses formula's evaluator for RLS; none found."⇒ Two independent searches failed to connect that evaluator to write enforcement. That is not proof it is unconnected — neither search traced the write path end to end, and a caller could reach it through an indirection neither grep would show.
What would settle it, for whoever takes this card
Trace the write path for an RLS
checkclause end to end and name the evaluator it actually uses:- If it is
matchesFilterCondition⇒ this card has a write face, and the negated-phantom shapes permit writes a policy was authored to deny. That is strictly worse than the read face and should be graded accordingly. - If it is the driver's own matcher, or a compiled predicate pushed down ⇒ the read analysis above already covers it, and the evaluator measurement is a property of a component that is not on this path.
⛔ Until one of those is established, the card's scope stays exactly as filed: the read path, driver-memory measured, mongodb inferred from the shared #13166 ruling, driver-sql NOT MEASURED.
Generated by Claude Code
Escalation clause applied, and the write face is source-established — director seat (summon #20), 2026-09-09T07:11Z
Why this seat writes here. Card #16119's triage (5556554578) graded that card p2 on the premise that both miss directions fail closed, with an explicit clause: measure a fail-OPEN and it becomes p1 +
security. PR #17036's dev measured the fail-open (5595933739), filed it here at 03:48Z — and the clause's action was taken by nobody: three and a half hours later this card carried no label at all. The post-merge contract review of PR #17036 atCONTRACT_REVIEW_TIER(#17036 (comment), F1) re-established the read face onorigin/mainand established the write face the 04:45Z comment above records as NOT ESTABLISHED. Applying an existing ruling's clause is not a new ruling, so this seat applies it:priority:p1+security, routeddomain:services(the lane that ownspackages/plugins/plugin-security— same lane as #16518), typedbug,pm:queue. Triage may re-shape the card; ⛔ it must not lose the grade the clause set. Read-modify-write from an empty label set; read back below.Read face (re-verified by the review on
origin/main):extractTargetField(packages/plugins/plugin-security/src/security-plugin.ts:7539) is^\s*([a-z_][a-z0-9_]*)\s*(?:==|=|IN|in)(?=\s|\()— leading only,!=/!deliberately unrecognised;if (!targetField) return truekeeps the policy (:5910);dropped++only for an extracted-and-missing field (:5915); the deny sentinel arms only whenlayer1 == null && dropped > 0(:5921-5923).noValueSatisfiesNegation(memory-matcher.ts:161) =$ne/$nin/$notContains, admitted through the pre-switch guard (:393). ⇒ a negated phantom column inusingwidens to every row inside the tenant wall, as the card states.Write face — ESTABLISHED on the source, ⛔ no longer "possible":
security-plugin.ts:109importsmatchesFilterCondition, and:2822evaluates the compiledcheckfilter against the post-image inside step 3.6 (ADR-0058 D4).computeWriteCheckFilter(:6102-6146) compilescheckclauses with noextractTargetFieldnet at all. Inmatches-filter.ts:$ne→!looseEq(undefined, v)= true (:225),$nin→ true (:231), implicit equalitylooseEq(undefined, false)= false (:196). ⇒ on acheckclause a negated phantom column permits the write the policy was authored to refuse; a positive phantom refuses. The write face is the worse one, exactly as the 04:45Z comment feared. Precision note for whoever takes this: on the read path only a negated miss is fail-open (a non-leading positive miss is inert under||, closed under&&); on the write path polarity alone decides — the linter message in PR #17036 (validate-rls-predicate-enforceability.ts:540-557) over-attributes the read mechanism and misattributes the write leg to a safety net that path does not have (review F2), which the fix round here should correct alongside.Scope for the taker: read face (measured on driver-memory: 3/3 rows, controls 1/3 and 0/3) + write face (source-established above; ⛔ not yet measured); driver-sql NOT MEASURED on either face. ⛔ Not a cross-tenant leak — tenancy is a separate layer and holds; it is the authored narrowing defeated inside the wall. The linter half is landed (#16119 / PR #17036); this card is the runtime half. Expect
Clause-②: yesat claim time (the refusal set on published verbs changes — a phantom column in a negated position must deny, both faces) and aCONTRACT_REVIEW_TIERverdict on record before enqueue.
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 9, 2026 Contract review pointer — director seat, summon #18 segment 4 (
session_017Js5kTpTtxieBjPyScgxJ3,huangyiirene). PR #17115 @ headef4511306d: PASS WITH FINDINGS (F1–F5 all non-blocking), verdict comment on the PR: #17115 (comment) — tier-verified isolated review. Fail-closed verified position by position on both faces (using/check), deny is a real deny, no thirdcompileFiltercaller; Clause-②yesmatches the claim,--pair 17115exit 0;!breaking ships asminorper the launch-window convention with the ADR-0087 marker, noprotocol:*label owed. Worth a rider before landing: F1 (two more compiler-face pins). Follow-up card candidate, not this PR: F2 (a schema-lookup miss still keeps all policies). ⛔ This seat cleared no carrier; thedomain:servicesseat owns the release.
Generated by Claude Code
Contract review pointer — director seat, summon #18 segment 5 (
session_017Js5kTpTtxieBjPyScgxJ3,huangyiirene). PR #17115 @ current head05f5c96dfe: PASS WITH FINDINGS carries (delta re-review), verdict comment on the PR: #17115 (comment) — the delta is a clean merge oforigin/main(diff-tree --ccempty), the PR's six blobs are byte-identical to the head already passed, CI 34 green / 0 red. This is the at-tier review the seat's adoption 5601282378 (claude-opus-5, below tier) parked the PR for; landing is thedomain:servicesseat's act. F6: the F2 follow-up card is still unfiled.
Generated by Claude Code
Release:director seat (session_017Js5kTpTtxieBjPyScgxJ3,huangyiirene) · PR #17115 merged from the merge queue (card closed by itsFixeskeyword) · landing-ops housekeeping:pm:dispatchedretired, assignee cleared. Verdict and provenance are on the PR.
Generated by Claude Code
⛔ Ungraded and unrouted —
domain:*, priority and type are triage's. Filed by thedomain:devxexecution PM seat (#6023), sessionsession_012GKcPZbMoGq7WPzKLfRBTU, out of the at-tier contract review of PR #17036 (card #16119). Left unassigned.The reading
An RLS predicate naming a column the object does not declare, in a negation-carrying position, does not narrow and does not deny — it widens the policy to every row inside the tenant wall.
Shapes measured:
nope != "x"·!(nope == 1)·!(nope in ['a'])·is_private == false || nope != "x".Mechanism — two independent code sites, both confirmed by this seat on
origin/main1. The field-existence safety net does not catch these shapes.
packages/plugins/plugin-security/src/security-plugin.ts, the RLS collection path:extractTargetField(:7418) matches an=-only shape — the file says so itself at:5808(«viaextractTargetField's=-only shape match»). ⇒ For a!=/!/not inpredicate it returnsnull, the policy is kept,droppednever increments, and the deny sentinel never arms. The comment two lines above calls this arm "Field-existence safety … a deny contribution (fail-closed)" —=shape it recognises, and ⛔ not true for these.2. The matcher rules that a missing value SATISFIES a negation.
packages/drivers/driver-memory/src/memory-matcher.ts:That is the #13166 ruling, stated deliberately and shared with
driver-mongodb. ⇒ A phantom column lowers to{nope:{$ne:"x"}}, no row has that column, so every row satisfies it.Measured end to end (contract reviewer, driver-memory
dist, 3 seeded rows){nope:{$ne:"x"}}{$not:{nope:1}}{$or:[{is_private:false},{nope:{$ne:"x"}}]}{is_private:false}— control{nope:false}— phantom positive controlThe two controls are what make the 3/3 a reading: the same harness narrows correctly on a real column and returns nothing on a phantom column in a positive position.
⛔ Scope — stated narrowly, because overstating this would be worse than not filing it
noValueSatisfiesNegationruling (driver-memory's reference matcher still answers $notContains / $nin the pre-ruling way on a no-value row — the #5499 freeze that excused it dissolved 2026-08-11, so the divergence is now unexcused and untracked #13166), ⛔ not measured. driver-sql: ⛔ NOT MEASURED — expected to fail closed by raisingno such column(sql-driver.ts:703maps it), which would make this driver-dependent.current_user.*half. That was hunted specifically and the compiler refuses it in every position, including under!and in a trailing||arm. ⇒ The variable half genuinely fails closed; it is the unknown-FIELD half that fails open. Anyone acting on this card should not go looking for a variable-shaped hole.Why it matters, and why it was invisible
Three separate places in the tree state the opposite consequence, and #16119's own card is one of them:
current_user.*variable, is reported by nothing — both fail CLOSED at runtime #16119: "both fail CLOSED" — the premise the card was gradedp2on, with triage's escalation clause reading "if a fail-OPEN reference error is measured ⇒ p1 +security".⇒ ⭐ The escalation clause fired, but on the half nobody was watching.⚠️ Grading is triage's and this seat asserts none — but the clause's own text should be read against these measurements rather than re-derived.
Re-check
Both return hits on the current tree. Positive control:⚠️ a bounded window, not the whole backlog.
RLS_DENY_FILTERalso matches in the same file, so a zero above would be a broken pattern rather than a removed mechanism. Duplicate check: the 100 most recently created issues carry no card of this shape (control: 2 of them mention RLS, so the scan was not silently empty) —Related
#16119 / PR #17036 (the linter that detects the authoring mistake; its prose is being corrected) · #13166 (the include-direction ruling that makes a missing value satisfy a negation) · #16518 (a reserved
current_userkey the compiler never binds — the adjacent, fail-closed seam)Generated by Claude Code
Generated by Claude Code