Permissions Inherit. Revocation Doesn't.

Granting access is a single write. Revoking it is a graph traversal nobody finished. The gap between those two operations is where most real-world access-control breaks actually live.

Every access-control system I've tested during red-team work, under NDA and outside it, breaks the same way eventually: not because the model of who's allowed to do what was wrong, but because granting access and revoking it were never built as the same operation. Granting is a single write: add a row, set a flag, issue a token. Revoking has to find every place that grant propagated to and undo it there too, and almost nobody builds revocation with the same care they build grant.

That asymmetry is the whole vulnerability class. It doesn't need a clever payload. It needs patience and a decent map of where a system's permission model and its permission implementation have quietly drifted apart.

Attack Vector: Stale Authorization After Revocation

Remove a user's access at the source of truth, the role table, the admin panel, the IdP group, and check what still works. Cached authorization tokens with a long TTL, session state that was hydrated before the change and never re-checked, downstream service mirrors that sync on a schedule instead of an event: all of these keep honoring a grant that no longer exists anywhere a human would look. The tell is a system with one visible place to manage permissions and three or four invisible places that also cache them. Test revocation the same way you'd test authentication: immediately, and again after the cache TTL you'd expect, and again after a service restart.

Attack Vector: Parent-ACL Overgrant

Permission systems love inheritance because it's convenient to administer: grant at the folder, tenant, or workspace level and let it cascade. The break shows up at the edges of that convenience. A child object created before a restrictive policy was applied to its parent often keeps its original, looser permissions, because the inheritance engine runs on writes, not retroactively on every existing descendant. Sharing links are the sharpest version of this: a link generated while a folder was public frequently keeps working after the folder is locked down, because the link is a capability, not a live permission check. Anywhere a system distinguishes "policy" from "the actual set of things that currently satisfy the policy," that gap is real and it's usually untested.

Attack Vector: Sibling-Tenant Credential Reuse

In multi-tenant systems, isolation is supposed to be a hard boundary, but it's frequently implemented as a soft one: a shared signing secret with a tenant ID embedded in the token payload rather than a per-tenant cryptographic boundary, or a database query that filters by tenant ID but trusts the caller to supply the right one instead of deriving it server-side from the authenticated identity. A session or API key minted for tenant A, replayed with tenant B's identifier substituted somewhere the client controls, validates cleanly because the signature only ever proved "this came from someone with a valid key," never "this came from someone authorized for this specific tenant." This is the multi-tenant version of an IDOR, and it's the first thing worth checking in any SaaS product built fast.

Blue Team Defense: Make Revocation an Event, Not a Cleanup Job

Every system that grants access needs a symmetric, tested code path that revokes it, propagated through every cache, mirror, and downstream consumer, on the same timeline as the grant itself. If granting access is instant and revoking it is "eventually, on the next sync," that gap is a standing vulnerability, not an acceptable latency tradeoff. Treat revocation propagation time as a monitored SLA, the same way you'd monitor authentication latency.

Blue Team Defense: Derive Tenant Scope Server-Side, Never From the Request

The fix for sibling-tenant reuse is structural: the server derives tenant scope from the authenticated identity and never trusts a tenant identifier supplied by the client, in a header, a body field, or a URL parameter, without cross-checking it against what that identity is actually authorized for. Per-tenant signing keys, not a shared secret with an embedded field, close this permanently instead of relying on every code path remembering to check correctly.

Blue Team Defense: Re-Evaluate Inherited Permissions, Don't Just Apply Them Forward

Inheritance engines that only run on write miss every object that existed before the policy changed. A periodic reconciliation pass, comparing what a policy currently says against what every existing object's actual permission state is, catches the drift that real-time inheritance never will. This is unglamorous, batch-job work, and it's the actual fix, not another layer of access-check middleware.

Blue Team Defense: Audit Capabilities, Not Just Policies

A sharing link, a long-lived API key, a cached token are capabilities: something that grants access independent of the current policy state. Any system that issues them needs an inventory of every live one and a way to invalidate them on demand, not just a policy engine that assumes the policy is the only thing that matters. If you can't answer "list every currently-valid way into this resource" in one query, you don't actually know your access-control surface, you know your access-control intent.

The access-control breaks that actually matter in production systems are almost never about the model being wrong. They're about the twenty small places implementation lagged the model, and revocation is where that lag shows up first, because it's the one operation nobody load-tested.


GhostInThePrompt.com // Revoking access didn't revoke the sharing links.