Ad-hoc role checks scattered across route files — user.role === 'admin' repeated in a dozen places — are impossible to audit and trivially out of sync. When a new role is added or a permission is tightened, every scattered comparison must be found and updated manually. One missed file is a silent privilege escalation. CWE-284 (Improper Access Control) and OWASP A01 (Broken Access Control) both trace back to exactly this pattern: authorization decisions made without a single, auditable source of truth. NIST 800-53 AC-3 requires access enforcement to be consistently applied — string comparisons in individual handlers fail that standard by design.
High because a missing or stale inline role check grants unauthorized capabilities to any authenticated user who reaches that route.
Create lib/auth/permissions.ts as the single source of truth for all role definitions and permission checks. Export a typed hasPermission(user, action) helper and import it from every route handler — never compare role strings inline again.
// lib/auth/permissions.ts
export const ROLES = { ADMIN: 'admin', MEMBER: 'member', VIEWER: 'viewer' } as const;
export type Role = typeof ROLES[keyof typeof ROLES];
export function hasPermission(user: { role: Role }, action: string): boolean {
const permissions: Record<Role, string[]> = {
admin: ['read', 'write', 'delete', 'manage_users'],
member: ['read', 'write'],
viewer: ['read'],
};
return permissions[user.role]?.includes(action) ?? false;
}
After adding this file, grep for user.role === across the codebase and replace every hit with a hasPermission() call.
ID: saas-authorization.access-control.auth-model-defined
Severity: high
What to look for: Count all relevant instances and enumerate each. Before evaluating, extract and quote any relevant configuration or UI text found. Look for a central authorization configuration file (e.g., lib/auth/permissions.ts, config/roles.ts), usage of RBAC libraries (casbin, accesscontrol, permit.io, casl), or a defined TypeScript enum or const object for roles and permissions. Check whether API routes and middleware import from a single source for authorization decisions rather than comparing raw strings inline.
Pass criteria: A centralized, documented authorization model exists — a specific file or library defines all roles and their permissions, and route handlers reference it rather than using ad-hoc string comparisons. At least 1 implementation must be verified. A partial or placeholder implementation does not count as pass.
Fail criteria: Authorization logic is purely ad hoc — raw string comparisons like user.role === 'admin' or user.isAdmin === true are scattered across route files with no central definition source.
Skip (N/A) when: Project has no authentication system detected (no auth library in dependencies, no session handling, no user concept in the data model).
Detail on fail: "Authorization logic is ad hoc. Found inline role comparisons in multiple route files with no central permissions definition." (Include the count of affected files if known.)
Remediation: Define a central source of truth for authorization. Create a lib/auth/permissions.ts file that exports your role definitions and a hasPermission(user, action) helper function. All route handlers should import from this file rather than comparing raw strings. This makes it easy to audit who can do what and ensures changes propagate everywhere.
// lib/auth/permissions.ts
export const ROLES = { ADMIN: 'admin', MEMBER: 'member', VIEWER: 'viewer' } as const;
export type Role = typeof ROLES[keyof typeof ROLES];
export function hasPermission(user: { role: Role }, action: string): boolean {
const permissions: Record<Role, string[]> = {
admin: ['read', 'write', 'delete', 'manage_users'],
member: ['read', 'write'],
viewer: ['read'],
};
return permissions[user.role]?.includes(action) ?? false;
}
After the change, verify by searching for raw role string comparisons (user.role === ) and converting them to hasPermission() calls.