When roles are checked with exact string equality (role === 'member'), a higher-privilege user fails checks designed for lower-privilege users — they must be explicitly listed in every condition they should satisfy. This creates access gaps that worsen as the role model grows. An Admin denied access to a Member-gated feature is a support burden at best; an Admin who can bypass a Member-gated resource because the hierarchy isn't enforced is a CWE-284 / OWASP A01 violation at worst. NIST 800-53 AC-3 expects access enforcement to reflect the intended privilege model, not accidental exclusion through literal comparisons.
High because exact-equality role checks produce both access gaps (Admins locked out) and privilege inversions (lower roles unexpectedly included) depending on how conditions are written.
Replace exact role comparisons with a hasMinimumRole helper that encodes the hierarchy numerically. Once the rank table exists, every future role check becomes a single function call.
// lib/auth/roles.ts
const ROLE_RANK: Record<string, number> = { viewer: 1, member: 2, admin: 3 };
export function hasMinimumRole(user: { role: string }, required: string): boolean {
return (ROLE_RANK[user.role] ?? 0) >= (ROLE_RANK[required] ?? Infinity);
}
// Usage: if (!hasMinimumRole(user, 'member')) return forbidden();
Review all existing user.role === 'x' checks and replace with hasMinimumRole(user, 'x') wherever hierarchy applies.
ID: saas-authorization.access-control.role-hierarchy-enforced
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 logic that compares role levels (e.g., an integer rank system where Admin=3 > Editor=2 > User=1) or explicit role inheritance definitions. Check if checks meant for a lower-privilege role correctly grant access to higher-privilege users — for example, does an "Editor can access this" check also pass for Admins, or does an Admin fail that check unless they're also explicitly listed?
Pass criteria: Roles are either defined hierarchically (higher roles encompass lower-role permissions) or permission checks use a helper that correctly handles hierarchy. At least 1 implementation must be verified. An Admin user passes all checks that a Member passes. A partial or placeholder implementation does not count as pass. Report the count even on pass.
Fail criteria: Roles are treated as mutually exclusive sets — an Admin user fails a check intended for Members because the check only matches role === 'member' literally, creating access gaps.
Skip (N/A) when: Only a single user role exists in the application, or no role hierarchy is implied by the data model.
Detail on fail: "Role checks use exact equality (===) rather than hierarchy. Admin users may fail checks intended for lower-privilege roles." (Note specific files where this pattern occurs.)
Remediation: Implement a hasMinimumRole helper instead of exact role equality checks. Define a role rank and check whether the user's role meets or exceeds the required minimum.
// lib/auth/roles.ts
const ROLE_RANK: Record<string, number> = { viewer: 1, member: 2, admin: 3 };
export function hasMinimumRole(user: { role: string }, required: string): boolean {
return (ROLE_RANK[user.role] ?? 0) >= (ROLE_RANK[required] ?? Infinity);
}
// Usage: if (!hasMinimumRole(user, 'member')) return forbidden();
Review all existing user.role === 'x' checks and replace with hasMinimumRole(user, 'x') where hierarchy applies.