Skip to content

measure: is a SESSION whose activeOrganizationId points at a left organization admitted the same way an ex-member API key was? — the membership guard is key-gated #15396

Description

@hotlong

Measurement only. ⛔ No fix lands under this card — the deliverable is a reading posted here. If it finds a hole, the repair is its own card under its own ruling.

The question

The organization_membership_ended guard in packages/core/src/security/resolve-authz-context.ts opens with:

if (keyPrincipal?.tenantId && input.tenancyPosture) {

It is API-key-gated. A session principal never reaches it. And a session's organization is also a stored claim — around line 361 of the same file:

tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;

So the two facts that made #15256 a p0 — the wall compares against a stored claim, and the fact that would expose an ended membership is not an input to the layer that could act on it — appear to hold for sessions as well, and #15365 did not touch that path: it made the posture available, but the guard that consumes it still only runs for API keys.

⚠️ Not asserted as a defect. better-auth's organization plugin may maintain activeOrganizationId itself — clearing or re-pointing it when a membership ends — in which case the claim is vetted at its source and there is nothing here. Nobody has looked. Both prior measurements (#15163 and cloud#1982) drove API keys only, by construction.

The measurement

On the same rig shape cloud#1982 used — a real objectstack serve boot of apps/objectos-ee under a live isolated posture with the real Organizations plugin mounted, OS_DATABASE_URL on a FILE so writes can be read back — with a session (cookie), not an API key:

  1. Sign in a user who has a sys_member row for org_alpha, and let the session's activeOrganizationId become org_alpha. Confirm it from the session record, not from the UI.
  2. Remove that membership (delete the sys_member row) while the session stays alive. ⛔ Do not sign out.
  3. Drive GET and POST on a plain org-stamped object (sys_business_unit served cloud#1982 well). Does the session still read org_alpha's rows? Does a write land, and does the row read back from the store carrying org_alpha?
  4. Repeat on the wiring where kernel is bound, if one can be booted — the API-key hole is closed there, and the question is whether the session hole is too.
  5. Read the code and state which layer, if any, re-vets activeOrganizationId after a membership ends: better-auth's own organization plugin, a hook in packages/organizations, or nothing.

Controls, both directions, or the run has measured nothing: a session whose membership is intact must still read and write (a rig that serves nobody proves nothing); and a signed-out request must still be refused. Seed rows in a second organization and assert they are never served.

Outcomes

Scope note

The wall-enforcing posture needs org-scoping, whose only registrar is the cloud-private packages/organizations — so this is an enterprise-surface question and the rig has to be a cloud one, as cloud#1982's was. ⛔ Do not substitute the framework's SimulatedOrgScopingPlugin stand-in and present it as the answer; state it plainly if that is all you can boot.

Refs: #15256 (the API-key hole and its ruling) · PR #15365 (a72704375) · #15163 · cloud#1982 · packages/core/src/security/resolve-authz-context.ts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions