Chapter 1 · Practice 1 of 3
Deny by default and require an explicit allow
Why it matters
OWASP's Authorization Cheat Sheet says an application "should be configured to deny access by default", and the Top 10:2025 lists "deny access by default, except for public resources" first among its prevention steps for A01.
In code, that means the absence of a rule is a denial:
- A new route without a permission annotation is rejected, not public.
- A role with no grant for an action cannot do it.
- An unknown resource type, action or attribute falls through to "deny".
Frameworks help or hurt here. Some make every route public until you add a guard. Flip that: install a global guard that denies, and mark the few public routes explicitly (@Public(), an allowlist of paths, a public: true flag). A forgotten annotation then fails safe, and a reviewer sees every public route in one place.
The same applies to data. A query helper that returns all rows unless you remember to filter is deny-by-default turned upside down. Prefer helpers that require a user or tenant argument (see the object-level chapter).
Example
// Every route needs a policy; routes without one are denied, not open
const routes = {
'GET /health': { public: true },
'GET /projects/:id': { action: 'project:read' },
'POST /projects/:id/archive': { action: 'project:archive' },
} as const;
export function guard(routeKey: string, user: User | null, resource?: unknown) {
const rule = routes[routeKey as keyof typeof routes];
if (!rule) return false; // unknown route: deny
if ('public' in rule) return true;
return user !== null && can(user, rule.action, resource);
}