No Privilege Escalation via Parameter Tampering
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.
// 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,updateUserServer Actions. Check how request body data is applied to the database update. Specifically: is...bodyorreq.bodyspread directly into adb.user.update({ data: body })call? Are sensitive fields likerole,permissions,isAdmin,plan,credits,subscriptionStatusin 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.// 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 — Improperly Controlled Modification of Dynamically-Determined Object Attributes
- cwe · CWE-285 — Improper Authorization
- owasp:2021 · A01
- nist:rev5 · AC-6
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-authorization·automated