# No Privilege Escalation via Parameter Tampering

- **Pattern:** `ab-002267` (`saas-authorization.admin-privilege.no-privilege-escalation-params`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002267
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002267)

## Why it matters

Spreading `req.body` directly into a `db.user.update({ data: body })` call is mass assignment (CWE-915): any field the attacker includes in the request body will be written to the database. For user records, this means `{ "role": "admin" }` is a single-request privilege escalation. CWE-285 (Improper Authorization) and OWASP A01 (Broken Access Control) cover this exact pattern. NIST 800-53 AC-6 (Least Privilege) requires that users cannot update fields beyond their entitlement — without an explicit allowlist, every field in the database schema is implicitly writable by authenticated users.

## Severity rationale

High because an authenticated user can escalate to admin privileges with a single crafted PATCH request, requiring no special knowledge beyond knowing which fields exist in the user model.

## Remediation

Parse update requests through a Zod schema that explicitly includes only safe profile fields. Zod silently strips any extra fields — including `role`, `permissions`, or `isAdmin` — before the data reaches the database.

```typescript
// lib/schemas/user.ts
export const ProfileUpdateSchema = z.object({
  displayName: z.string().min(1).max(50).optional(),
  bio: z.string().max(500).optional(),
  avatarUrl: z.string().url().optional(),
  // 'role', 'permissions', 'isAdmin' are intentionally absent
});

// app/api/users/[id]/route.ts
export async function PATCH(req: NextRequest, { params }) {
  const session = await auth();
  if (!session || session.user.id !== params.id) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }
  const data = ProfileUpdateSchema.parse(await req.json()); // Strips sensitive fields
  await db.user.update({ where: { id: params.id }, data });
}
```

Add `.strict()` to the schema to reject (rather than silently strip) requests that include unexpected fields — this surfaces integration bugs faster.

## Detection

- **ID:** `no-privilege-escalation-params`
- **Severity:** `high`
- **What to look for:** Count all relevant instances and enumerate each. Find user update endpoints — `PATCH /api/users/[id]`, `PUT /api/profile`, `updateUser` Server Actions. Check how request body data is applied to the database update. Specifically: is `...body` or `req.body` spread directly into a `db.user.update({ data: body })` call? Are sensitive fields like `role`, `permissions`, `isAdmin`, `plan`, `credits`, `subscriptionStatus` in the update schema? This is a mass assignment vulnerability.
- **Pass criteria:** User update endpoints use an explicit allowlist of updatable fields (via Zod schema or manual field selection) that excludes sensitive fields. At least 1 implementation must be verified. The request body cannot contain `role`, `permissions`, `isAdmin`, or similar fields that would be applied.
- **Fail criteria:** Any user update endpoint spreads the request body directly into a database update without filtering out sensitive fields, or a Zod schema for user updates includes `role`, `permissions`, or similar fields without requiring admin authority.
- **Skip (N/A) when:** No user update endpoints found in the codebase.
- **Detail on fail:** `"User update at [route] may allow mass assignment. Sensitive fields (role, permissions, isAdmin) could be updated via the request body."` (Note the specific file and the pattern used.)
- **Remediation:** Always use an explicit field allowlist for user updates. Zod's `.pick()` or `.omit()` methods are the easiest way to enforce this.

  ```typescript
  // lib/schemas/user.ts
  // This schema only allows safe profile fields — no role, no permissions
  export const ProfileUpdateSchema = z.object({
    displayName: z.string().min(1).max(50).optional(),
    bio: z.string().max(500).optional(),
    avatarUrl: z.string().url().optional(),
  });

  // app/api/users/[id]/route.ts
  export async function PATCH(req: NextRequest, { params }) {
    const session = await auth();
    if (!session || session.user.id !== params.id) {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
    }
    const data = ProfileUpdateSchema.parse(await req.json()); // Strips any extra fields
    await db.user.update({ where: { id: params.id }, data });
  }
  ```

  The key is that `ProfileUpdateSchema.parse()` will strip any fields not in the schema — so even if an attacker sends `{ "role": "admin" }`, Zod will discard it silently (or throw if `.strict()` is used).

## External references

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

Taxons: access-control, injection-and-input-trust

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