Origin
Resolution of audit finding HC-1 (2026-04-07 authority-hierarchy lockdown). Supersedes the earlier FORBIDDEN "Access denied" pattern. The prior pattern leaked entity existence to cross-organization probes — an attacker could distinguish “entity exists in another organization” from “entity doesn’t exist” by the error code returned, enabling enumeration attacks across organizational boundaries.
Rule Text
Every single-entity fetch MUST include organizationId = ctx.session.activeOrganizationId in the SQL WHERE clause. If the query returns zero rows, throw NOT_FOUND "<Entity> not found". Never use FORBIDDEN "Access denied" — that error leaks entity existence.
Testable Assertion
// For any request crossing an organization boundary, regardless of whether
// the entity exists, is soft-deleted, or belongs to another organization:
expect(error.code).toBe("NOT_FOUND");
expect(error.message).toBe("<Entity> not found");
Enforcement
- Write-time — Hookify rule
block-access-denied-forbiddenblocks the literal stringFORBIDDEN "Access denied"from being written in any procedure file. Rule fires at the Edit/Write tool call. - Gate-time — ast-grep pattern
require-org-scope-in-whererequires theorganizationId = ctx.session.activeOrganizationIdpredicate in every WHERE clause touching an organization-scoped table. Runs inpnpm gate. - Runtime — The
propertyProceduremiddleware (see AUTH-2) verifies session active organization before the procedure body executes. Failure short-circuits withUNAUTHORIZEDbefore the WHERE clause is reached.
Violation Closed
Three distinct failure cases — entity does not exist, entity was soft-deleted, entity belongs to another organization — collapse into a single indistinguishable response. A probing client observes identical output for all three scenarios. Enumeration across organizational boundaries ceases to be possible. The rule turns a class of information-disclosure vulnerability into a form the attacker cannot distinguish from a correctly-operating system.