Security recipes · 08

Cross-org privilege escalation

The threat
Use legitimate authority in one organization — admin rights, broad role grants, a switched org context — to read or mutate another organization's users, roles or resources.
Sentinel's counter
The effective-org evaluation rule fences cross-org checks to explicitly org-restricted grants, delegated admin reach is derived structurally from managed-org visibility, and org-role grants are validated tighten-only at write time.

The attack

Multi-org systems fail sideways. The attacker is rarely an outsider — it’s an org admin, a contractor with delegated rights in one tenant, or a compromised account with a broad-looking role, discovering that authority granted somewhere works everywhere. The classic shapes:

  • Confused-deputy reads: an admin API that checks “is caller an admin?” but not “an admin of the org this resource belongs to?”
  • Context smuggling: switch your token’s org context to an org you don’t belong to, or aim an org-scoped API at another org’s resource id and hope the check only looks at the permission name.
  • Grant minting: use delegated role-editing rights in org A to create a role whose grants point at org B — or at the whole realm — and assign it to yourself.

Each is an authorization bug that unit tests on single-tenant fixtures will never catch, because every individual permission check passes. The failure is in which organization the check was evaluated against.

How Sentinel counters it

The effective-org rule is in the evaluator, not in call sites. AuthorizationEvaluator.Evaluate computes the effective organization once: the resource’s org when the check states one, else the token’s org context. When the two differ — a cross-org check — the fence engages inside ScopeApplies:

if (crossOrg && grant.OrganizationId is null)
{
    return false;
}

A realm-wide allow is not a license to reach across organizations the token was not minted for; only grants explicitly restricted to the resource’s org apply to a cross-org check. Because point checks and list visibility share these exact primitives, a caller can never enumerate rows the row-by-row evaluation would deny — and deny-overrides still runs on top, so a matching deny kills the check regardless of org gymnastics.

Org context is earned, not asserted. The org claim enters a token through login or through OrgSwitchService, which validates membership against the live store, not the presented token — a user removed from an org since login cannot switch into it. Each (user, org) pair gets its own permission snapshot; there is no ambient “all my orgs at once” context.

Delegated admin reach is structural. SentinelAdminService derives what an org admin can touch from their managed-org set — as the test suite’s own preamble puts it, enforcement is “never by an org comparison in the service”; targets outside the caller’s reach are simply not found. The admin fencing tests stage the attacks directly: Org_admin_cannot_list_get_or_suspend_users_in_another_org, Org_admin_cannot_touch_another_orgs_record, and — the subtle one — Role_create_and_assign_in_unmanaged_org_fail_even_with_org_manage_elsewhere: holding org manage in org A buys exactly nothing in org B. Over the wire, Org_admin_gets_200_on_their_org_and_403_admin_scope_on_the_other pins the same fence end to end, and not yours answers identically to not there — no enumeration oracle.

Grant minting is fenced at write time, tighten-only. Org-scoped role editing cannot forge broader authority than the editor holds: Org_role_grants_cannot_mint_global_scope_or_point_at_other_orgs — an org role’s grants are validated to stay inside that org, and Org_admin_cannot_create_realm_level_role closes the vertical variant. Malformed grant patterns are rejected before they’re stored at all. When you need to see why a decision fell the way it did, the AuthorizationInspector returns the full evaluation trace — and inspecting an unmanaged org’s context is itself denied (Inspecting_an_unmanaged_org_context_is_denied).

What your app must still do

  • State the resource’s org on your checks. The fence keys off AccessCheck.ResourceOrganizationId. For any resource that belongs to an organization, populate it from your store — the fence can only guard checks that declare whose resource is being touched.
  • Derive org ids server-side. Resolve the org from the authenticated context or the resource row, never from a client-supplied header or body field that bypasses the token’s org claim.
  • Model tenants as organizations. The machinery guards Sentinel’s org axis. A tenant id kept as a free-form user attribute gets none of this — orgs, memberships and org-scoped roles are the load-bearing structures.
  • Review cross-org grants like the exceptions they are. The escape hatch — a grant explicitly restricted to a foreign org — exists for real delegation scenarios. Treat creating one as a privileged, audited act; the admin audit chain records it with before/after payloads.