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
The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape #14137
Filed by the domain:services PM seat (#6021) from the contract re-review of PR #14129 (#14029), recorded there as a non-blocking finding and deliberately not folded into that PR.
The residue
#14029 gave MANAGED_DENY_TARGET_SETS an independent-property pin: derive the required floor from the real seeded defaultPermissionSets — "a default set whose '*' wildcard grants a write" — and diff it against the list, so a set that should be a member cannot hide behind an assertion that merely restates the list.
The modifyAllRecords clause was added by the first review, which caught that the evaluator grants writes by a second route (permission-evaluator.ts:224, MODIFY_ALL_WRITE_KEYS).
⚠️There is a third. The evaluator's first route is a direct bit read off OPERATION_TO_PERMISSION:
and allowTransfer is a real, granted, enforced bit — packages/spec/src/security/permission.zod.ts:193, .default(false), ENFORCED under #3004. Its docblock in the evaluator says so in as many words: "dispatched through the security middleware it is gated by allowTransfer — deny unless a resolved permission set grants it."
⇒ A future default set shaped '*': { allowRead: true, allowTransfer: true } holds transfer — reassigning the owner — on every future managedBy: 'better-auth' identity table, while all three write flags andmodifyAllRecords are false. Neither floor clause fires, so it is not required to be a managed-deny target and keeps its wildcard silently.
Reverse control, so the reading is legible: the same grep shape over the same file returns update: 'allowEdit' at :29 — the mapping table is live and really is how operations reach permission bits.
Why this is p3 and not p2
⛔ No such set exists today. Every current write-granting default set trips one of the existing clauses, so the derived floor is unchanged and there is zero behaviour delta.
⭐ It is worth a card rather than a note because it is the third size of one shape: "a set that should be a member can hide." First the set was missing from the list; then the floor missed modifyAllRecords; now it misses allowTransfer. Each layer is visible only by reading the evaluator's grant semantics, never by reading the flags on the wildcard.
⚠️Consider deriving the floor from OPERATION_TO_PERMISSION / MODIFY_ALL_WRITE_KEYS instead of restating bits by hand. Three hand-maintained lists have now drifted from the evaluator in three successive rounds. Deriving from the evaluator's own tables would end the class rather than shrink it a fourth time — but it is a bigger change and wants its own judgement, which is why this card suggests rather than rules.
Not established
⛔ Whether any planned or downstream permission set intends a transfer-only wildcard. Not measured; this is a pin-completeness gap, not a report of a live grant.
⛔ Whether other enforced bits outside OPERATION_TO_PERMISSION and MODIFY_ALL_WRITE_KEYS also bypass the floor. ⚠️ A full census of the evaluator's grant routes has not been done — this finding came from one route the reviewer happened to read. That census is the honest prerequisite for option 3.
Related: #14029 / PR #14129 (the card and its two review rounds) · #3325, ADR-0092 (registry-driven managed-object write denies) · #3004 (allowTransfer enforcement) · ADR-0066 D2
Filed by the
domain:servicesPM seat (#6021) from the contract re-review of PR #14129 (#14029), recorded there as a non-blocking finding and deliberately not folded into that PR.The residue
#14029gaveMANAGED_DENY_TARGET_SETSan independent-property pin: derive the required floor from the real seededdefaultPermissionSets— "a default set whose'*'wildcard grants a write" — and diff it against the list, so a set that should be a member cannot hide behind an assertion that merely restates the list.That floor is now:
The
modifyAllRecordsclause was added by the first review, which caught that the evaluator grants writes by a second route (permission-evaluator.ts:224,MODIFY_ALL_WRITE_KEYS).OPERATION_TO_PERMISSION:and
allowTransferis a real, granted, enforced bit —packages/spec/src/security/permission.zod.ts:193,.default(false), ENFORCED under #3004. Its docblock in the evaluator says so in as many words: "dispatched through the security middleware it is gated byallowTransfer— deny unless a resolved permission set grants it."⇒ A future default set shaped
'*': { allowRead: true, allowTransfer: true }holds transfer — reassigning the owner — on every futuremanagedBy: 'better-auth'identity table, while all three write flags andmodifyAllRecordsare false. Neither floor clause fires, so it is not required to be a managed-deny target and keeps its wildcard silently.Reverse control, so the reading is legible: the same grep shape over the same file returns
update: 'allowEdit'at:29— the mapping table is live and really is how operations reach permission bits.Why this is p3 and not p2
modifyAllRecords"), so the docblock is now true within its own stated definition. What is left is a floor that is narrower than the evaluator's full grant surface — a gap, not a lie.modifyAllRecords; now it missesallowTransfer. Each layer is visible only by reading the evaluator's grant semantics, never by reading the flags on the wildcard.Suggested shape (not a prescription)
|| wc.allowTransfer === true.=== true), never a key-existence test — Zod materialises these bits with.default(false), so they are present-as-false and an existence test misfires today. (Established in fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129's first review; do not re-litigate it.)OPERATION_TO_PERMISSION/MODIFY_ALL_WRITE_KEYSinstead of restating bits by hand. Three hand-maintained lists have now drifted from the evaluator in three successive rounds. Deriving from the evaluator's own tables would end the class rather than shrink it a fourth time — but it is a bigger change and wants its own judgement, which is why this card suggests rather than rules.Not established
OPERATION_TO_PERMISSIONandMODIFY_ALL_WRITE_KEYSalso bypass the floor.Related: #14029 / PR #14129 (the card and its two review rounds) · #3325, ADR-0092 (registry-driven managed-object write denies) · #3004 (
allowTransferenforcement) · ADR-0066 D2Generated by Claude Code