# Role Assignment Restricted

- **Pattern:** `ab-002242` (`saas-authorization.access-control.role-assignment-restricted`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002242
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002242)

## Why it matters

A user update endpoint that accepts a `role` or `isAdmin` field in the request body without admin verification is a mass assignment vulnerability (CWE-915) enabling privilege escalation (CWE-285). Any authenticated user who POSTs `{ "role": "admin" }` to their own profile endpoint will elevate their privileges if the backend spreads the request body directly into a database update. OWASP A01 (Broken Access Control) flags this pattern explicitly. NIST 800-53 AC-6 (Least Privilege) requires that users cannot grant themselves permissions beyond what they were assigned.

## Severity rationale

Low because the attacker must be authenticated and must know which fields to tamper with, but self-promotion to admin via a single request is a complete authorization bypass.

## Remediation

Define a profile update Zod schema that does not include `role`, `permissions`, or any privilege field. Handle role changes through a separate admin-only endpoint.

```typescript
// lib/schemas/user.ts
export const UserProfileUpdateSchema = z.object({
  displayName: z.string().optional(),
  bio: z.string().optional(),
  // 'role' is intentionally absent
});

// app/api/admin/users/[id]/role/route.ts — separate admin-only endpoint
export async function PATCH(req: NextRequest, { params }) {
  const session = await auth();
  if (session?.user.role !== 'admin') return NextResponse.json({}, { status: 403 });
  const { role } = await req.json();
  await db.user.update({ where: { id: params.id }, data: { role } });
}
```

`UserProfileUpdateSchema.parse()` silently drops any fields not in the schema, so attackers cannot inject privilege fields even if they try.

## Detection

- **ID:** `role-assignment-restricted`
- **Severity:** `low`
- **What to look for:** Count all relevant instances and enumerate each. Find API routes or Server Actions that update user records — `PATCH /api/users/[id]`, `updateUser`, `updateProfile`, `inviteMember`. Check whether these endpoints accept a `role` or `permissions` field in the request body. If they do, check whether the handler verifies that the requester has admin-level authority to set that field before applying the change.
- **Pass criteria:** Endpoints that update user data either (a) do not accept role/permissions fields at all in their input schema, or (b) explicitly verify the requester holds admin-level authority before applying a role change. At least 1 implementation must be verified.
- **Fail criteria:** A user update endpoint accepts a `role`, `permissions`, `isAdmin`, `plan`, or similar field in the request body and applies it without checking that the requester has administrative authority to make that change.
- **Skip (N/A) when:** No user update endpoints or role management functionality detected.
- **Detail on fail:** `"User update endpoint accepts role or permission fields without verifying admin authority. Mass assignment may allow self-promotion."` (Note the specific files.)
- **Remediation:** Use Zod (or your validation library) to define an update schema that explicitly excludes sensitive fields. Handle role changes through a separate, admin-only endpoint.

  ```typescript
  // lib/schemas/user.ts
  // Profile updates — role field is absent from this schema
  export const UserProfileUpdateSchema = z.object({
    displayName: z.string().optional(),
    bio: z.string().optional(),
    // 'role' is intentionally NOT here
  });

  // app/api/admin/users/[id]/role/route.ts — separate admin-only endpoint
  export async function PATCH(req: NextRequest, { params }) {
    const session = await auth();
    if (session?.user.role !== 'admin') return NextResponse.json({}, { status: 403 });
    const { role } = await req.json();
    await db.user.update({ where: { id: params.id }, data: { role } });
  }
  ```

- **Cross-reference:** For related patterns and deeper analysis, see the corresponding checks in other AuditBuffet audits covering this domain.

---

## External references

- cwe CWE-285
- cwe CWE-915
- owasp A01
- nist AC-6

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002242
