Skip to content

fix(security)!: a disabled RLS policy no longer grants — security-subset liveness re-verification (#3896 follow-up) - #3980

Merged
os-zhuang merged 1 commit into
mainfrom
claude/sharing-rules-schema-bypass-h4c7xr
Jul 30, 2026
Merged

fix(security)!: a disabled RLS policy no longer grants — security-subset liveness re-verification (#3896 follow-up)#3980
os-zhuang merged 1 commit into
mainfrom
claude/sharing-rules-schema-bypass-h4c7xr

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

The security-subset re-verification of the liveness ledger, and the defect it found. Timed for the v17 window on purpose: if an audit like this turns up false compliance, the remedies (enforce / remove) are exactly the changes a major-release RC makes cheap.

The defect: rowLevelSecurity.enabled was a no-op, in the over-share direction

RowLevelSecurityPolicySchema.enabled promises, verbatim:

"Whether this policy is currently active. Disabled policies are not evaluated. Useful for temporary policy changes without deletion."

Nothing read it. Not the collection site (security-plugin.ts pushes every policy, filtering only platform-tenant provenance), not the projection round-trip (verbatim JSON), not the compiler (getApplicablePolicies matched on object / positions / operation only).

Direction matters: applicable policies OR-combine — "any match allows access" (rls-compiler.ts's own doc). Each policy is a permissive grant widening row visibility. So a policy an admin switched off kept contributing its grant — disabling a too-permissive policy silently changed nothing. That is the #3896 shape one layer up: a documented security control whose real behaviour is wider than its written contract, discoverable only by reading the code.

The ledger, meanwhile, claimed live with evidence pointing at rls-compiler.ts — a file that never mentioned the property. Exactly the "claim with a timestamp; code moves under it" failure the ledger README warns about, on the entry class where it costs most.

The fix

getApplicablePolicies now excludes enabled === false before any matching — the single choke point both the find path and the analytics path flow through, and the same spot where the formerly-unenforced positions domain got the identical ADR-0049 enforce-or-remove treatment. Exact === false on purpose: the schema defaults enabled to true and rows round-tripped through sys_permission_set may omit the key, so absent stays active — no legacy row changes behaviour.

Access-narrowing only: no policy grants more after this change. Nothing in-repo authors enabled: false (checked examples/seeds), and the Studio permission form doesn't render a toggle for it, so no tenant flips it expecting the old (broken) behaviour. Four tests pin both directions, including "a disabled policy no longer contributes its OR-branch to the compiled filter".

The audit around it

All 44 entries of the ledger's security subset — permission (33), position (4), object sharing/access (7) — call-graph-closed by hand per the README method (declaration → registration → accessor → non-test caller; grep only proves presence) and stamped verifiedAt: 2026-07-30. Repo-wide dated entries: 4 → 48. This subset had never been re-verified; the last sweep that leaned on preview-renderer citations went 10-for-13 wrong.

Corrections beyond enabled:

Entry Was Now Why
rls.priority live (evidence never read it) dead + authorWarn Semantically void, not merely unimplemented: OR-combination means most-permissive-wins (the schema's own describe says so), so there is no conflict to order and evaluation order cannot affect outcomes. Remove candidate per the #3715/#3950 precedent — flagging for the v17 window, not done here.
rls.label / description / tags live, no evidence dead (benign) No consumer in either repo. objectui's facets editor and preview echo authored input — an authoring surface is not a runtime consumer (README §preview-renderer). Not authorWarn'd: benign display metadata.
tabPermissions live, no evidence, note "only 'hidden' is read" live, evidence + corrected note UNDERSTATED: hono's tabRank merge reads all four visibility values across resolved sets. The me-apps-and-everyone-baseline dogfood test exercises it; noted as a proof-binding candidate (the proof registry currently records it against the ungoverned app type).
object.ownership live, evidence registry.ts:272 live, registry.ts:292 Line rot — the citation no longer pointed at the read.

Suspicions the audit REFUTED (worth recording so nobody re-suspects them):

  • allowExport is not projection-only. The export route carries its own caller-level 403 gate (enforceExportPermissioncanExportcheckObjectPermission('export')), fail-closed when the security service is present but cannot answer, separate from the object-level 405. The code comment even names the hazard: "without this the bit would only hide the client's Export button while curl still drained the table."
  • allowTransfer / allowRestore / allowPurge notes are accurate: M2 operations still unshipped (no such ops in packages/objectql), and the RBAC gates are pre-mapped with a DESTRUCTIVE_OPERATIONS fail-closed backstop, so they cannot ship ungated.

position.json: all four entries verified, no corrections. object.json: seven security entries re-cited and stamped (sharingModel's fail-closed default — no sharingModel + not system ⇒ private — confirmed in sharing-service.ts).

Verification

  • plugin-security: 32 files / 677 tests green (4 new).
  • tsc --noEmit clean, eslint clean on touched files.
  • check:liveness green — 48 dated entries, every evidence path resolves, all bound proofs resolve.
  • README counts table regenerated for the permission row (29 live / 4 dead).

For the v17 triage (decisions this PR flags but does not take)

  1. Remove rls.priority? Semantically void; the breaking window is open now. Small schema-removal PR on request.
  2. Bind permission.tabPermissions to the me-apps proof once its authored surface is confirmed to be the permission-set side (ADR-0054 "bind only what the proof authors").
  3. writeScope proof — sibling of proofed readScope, stamped live with two confirmed readers but no dogfood proof of its own.

Generated by Claude Code

…up security-subset audit)

RowLevelSecurityPolicySchema.enabled promises, verbatim: "Disabled policies
are not evaluated." Nothing read it — not the collection site, not the
projection round-trip, not the compiler. Because applicable policies
OR-combine (any match allows access), a policy an admin switched off kept
contributing its grant: disabling a too-permissive policy silently changed
nothing. The #3896 shape one layer up — a documented security control whose
real behaviour is wider than its contract.

getApplicablePolicies now excludes enabled === false before any matching, at
the single choke point both the find and analytics paths flow through — the
same place, and the same ADR-0049 enforce-or-remove resolution, as the
formerly-unenforced positions domain. Exact === false on purpose: the schema
defaults enabled to true and projection rows may omit the key, so absent
stays active. Four tests pin both directions. Access-narrowing only; nothing
in-repo authors enabled:false.

Found by re-verifying the liveness ledger's security subset: all 44 entries
(permission 33, position 4, object sharing/access 7) call-graph-closed by
hand and stamped verifiedAt — the subset's first-ever re-verification.
Other corrections:

- rowLevelSecurity.priority → dead + authorWarn: under OR-combination the
  promised "conflict resolution" semantics cannot exist. REMOVE candidate
  (#3715/#3950 precedent) while the v17 breaking window is open.
- rowLevelSecurity.label/description/tags → dead (benign display, no
  consumer in either repo, not authorWarn'd).
- tabPermissions was UNDERSTATED ("only 'hidden' is read" → the rank merge
  reads all four values; the me-apps dogfood test exercises it). Evidence
  upgraded; proof-binding candidate.
- allowExport re-verified TRUE against the projection-only suspicion: the
  export route carries its own caller-level 403 gate, fail-closed.
- allowTransfer/Restore/Purge notes re-confirmed (M2 still unshipped).
- object.ownership evidence line had rotted — refreshed; 6 more object
  security entries re-cited and stamped.

plugin-security: 32 files / 677 tests green. tsc clean, eslint clean,
check:liveness green (48 dated entries, all paths resolve).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 12:51am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-security, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-security, packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 01:20
@os-zhuang
os-zhuang merged commit 94a0bbc into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/sharing-rules-schema-bypass-h4c7xr branch July 30, 2026 01:20
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…rity, bind tabPermissions + writeScope proofs (#3990)

The three items #3980 flagged for the v17 window.

1. rls.priority removed — it promised "conflict resolution" that cannot exist: applicable policies OR-combine (most permissive wins, per the schema's own describe), so there is never a conflict to order and nothing ever read the key. Tombstoned via retiredKey (tsc never + parse-time prescription, both directions pinned), stripped mechanically by the permission-rls-priority-removed D2 conversion + D3 chain step, cleaned from factory helpers / the showcase example / the RLS docs, baselines updated. The docs table's enabled row now states the since-#3980-enforced contract.

2. permission.tabPermissions bound to the me-apps proof — after extending the proof to actually AUTHOR the property on the permission-set side (it had only mentioned tabPermissions in its header; binding then would have been the owner-anchor/allowTransfer anti-pattern). The new case pins hidden dropping the app from /me/apps, a more-visible grant winning it back (tabRank most-visible-wins), and unrelated principals untouched.

3. writeScope gets its runtime proof (showcase-scope-depth-write, 9/9, post-state assertions): 'unit' edits a BU co-member's row and does not descend; an ABSENT writeScope stays owner-only even under a 'unit' readScope (the axes gate independently); the hierarchy seam fails closed without the enterprise resolver. Bound as the scope-depth-write class.

spec: 6879+ tests green incl. the chain-replay composability gate (union-merged over #3987's protocol-17 conversions); all gates green; dogfood fixture sweep corrected (the invoice-seed-isolation fixture no longer authors the retired key).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants