PCI-DSS 4.0 Req 7.1 requires a formal model where access to system components is restricted on a need-to-know basis; Req 7.2 mandates that the access control model documents each user's required access level. Without RBAC, every authenticated user is implicitly authorized to access all data — a customer account can query transaction records for other customers, or a support agent can issue refunds. CWE-285 (Improper Authorization) and OWASP A01 (Broken Access Control) are the most exploited vulnerability class across web applications. NIST AC-2 requires that accounts are managed with assigned access rights enforced in code.
High because missing or unenforced role separation means any authenticated user can access data or perform actions beyond their authorization scope, directly violating PCI-DSS 4.0 Req 7.2 and enabling horizontal privilege escalation.
Define roles in a docs/rbac.md permissions matrix (minimum: admin, merchant, customer), then enforce them in middleware — not inline in each handler. Create a requireRole() helper in src/middleware/auth.ts that returns 403 before any business logic runs.
// src/middleware/auth.ts
export async function requireRole(supabase, requiredRoles: string[]) {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response('Unauthorized', { status: 401 });
const { data } = await supabase.from('user_profiles')
.select('role').eq('user_id', user.id).single();
if (!requiredRoles.includes(data.role))
return new Response('Forbidden', { status: 403 });
return null;
}
Apply it at the top of every API route that touches CDE data before any database query executes.
ID: ecommerce-pci.access-control.rbac-enforced
Severity: high
What to look for: Count all distinct roles defined in the project (database schema columns, auth config, middleware, documentation). For each role found, check whether permission enforcement exists in at least 1 API route or middleware. Count the number of API routes that include role/permission checks. Look for a documentation file (e.g., docs/rbac.md) that defines at least 3 roles with a permissions matrix.
Pass criteria: At least 3 distinct roles are defined (e.g., admin, merchant, customer). Permissions are enforced in code via middleware or guards in at least 2 API routes. A documentation file exists with a permissions matrix listing at least 3 roles and at least 4 permissions. Report the count: "X roles defined, Y routes with permission checks, Z documentation files."
Fail criteria: Fewer than 3 roles defined, or roles are defined but not enforced in code (0 permission checks in API routes), or no documentation explains role responsibilities. Must not pass when roles exist only in a database column with no code enforcement.
Skip (N/A) when: Application is single-user with no multi-user access (only 1 user type, no auth system, no admin panel).
Detail on fail: Specify what's missing. Example: "Database has user_role column with 2 values but no permission checks in any of 5 API routes. 0 middleware guards found." or "4 roles defined and enforced but RBAC documentation not found."
Cross-reference: See ecommerce-pci.access-control.mfa-enforced (MFA for admin), ecommerce-pci.access-control.service-accounts-least-privilege (service account scope).
Remediation: Implement and document RBAC. Create a permissions matrix:
# RBAC Definition (docs/rbac.md)
## Roles
- **admin**: Full system access, user management, compliance
- **merchant**: Access to own transactions, reports, settings
- **finance**: Access to settlement reports and payment records
- **customer**: Access to own orders and payment history
- **support**: Read-only access to customer data, no payment modifications
## Permissions Matrix
| Permission | Admin | Merchant | Finance | Customer | Support |
|---|---|---|---|---|---|
| View all transactions | Yes | No | Yes | Own only | No |
| Refund transactions | Yes | Own only | Yes | No | No |
| View financial reports | Yes | Own only | Yes | No | No |
| Access CDE | Yes | No | Yes (limited) | No | No |
Implement in code:
// middleware/auth.ts
import { createClient } from '@/utils/supabase/server';
export async function requireRole(req, requiredRoles) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response('Unauthorized', { status: 401 });
const { data: userProfile } = await supabase
.from('user_profiles')
.select('role')
.eq('user_id', user.id)
.single();
if (!requiredRoles.includes(userProfile.role)) {
return new Response('Forbidden', { status: 403 });
}
return null; // Allowed
}
// src/app/api/transactions/route.ts
export async function GET(req) {
const auth = await requireRole(req, ['admin', 'merchant', 'finance']);
if (auth) return auth;
// Permission granted
return new Response(JSON.stringify(transactions));
}