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.
- Capability
- /docs/authorization/

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
orgclaim. - 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.