Permission and Authorization Bugs

Mass Assignment Allows Role Escalation

Any authenticated user can promote their own account to admin by including a 'role' field in the body of a PATCH /api/users/me request. The endpoint deserialises the entire request body onto the user record without an allowlist, so the role field is treated as a normal updatable field rather than a protected one.

CriticalIntermediateSecurity testingAPI testingManual testing

// UNDERSTAND

// Symptoms

  • A standard user sends PATCH /api/users/me with { "role": "admin" } and the response body shows role: 'admin'
  • After the PATCH, the same user can call admin-only endpoints that previously returned 403
  • The role field appears in the user object returned by GET /api/users/me after the update
  • No 403 Forbidden or 422 Unprocessable Entity is returned when a non-admin submits a role field

// Root Cause

  • The PATCH handler passes the raw request body to the ORM's update method (e.g. User.update(userId, req.body)) without an allowlist of updatable fields. Every key in the payload โ€” including role โ€” is written to the database.
  • A field-level allowlist that restricts which fields a user can update on their own record is absent from the handler. The role field is not marked as non-writable via the API, so it is treated identically to display_name or email.

// Where It Appears

  • User profile update endpoints that accept a JSON body without explicit field filtering
  • REST APIs generated by frameworks or ORMs that auto-bind request body properties to model attributes
  • Multi-tenant SaaS applications where role and plan fields are stored on the same user record as profile data
  • Any PATCH or PUT endpoint where the allowed-fields list was not explicitly defined

// REPRODUCE & TEST

// How to Reproduce

  1. 01Log in as a standard user and obtain the bearer token
  2. 02Send GET /api/users/me with the bearer token and record the current role field value (e.g. 'viewer')
  3. 03Send PATCH /api/users/me with body { "role": "admin" } and the same bearer token
  4. 04Read the role field in the PATCH response body
  5. 05Send GET /api/users/me again and confirm the role field is now 'admin'

// Test Data Needed

  • A standard user account (not an admin) with a valid bearer token
  • A way to send a raw PATCH request with a controlled body (Postman or curl)

// Manual Testing Ideas

  • Submit a PATCH /api/users/me with { "role": "admin" } and observe whether the role changes
  • Also try other privileged fields: { "isAdmin": true }, { "plan": "enterprise" }, { "credits": 99999 } โ€” mass assignment often affects multiple fields
  • After a successful role escalation, test whether the elevated role actually grants admin API access by calling an admin-only endpoint
  • Confirm the field is truly protected by verifying that an explicit admin PUT /api/admin/users/{id} endpoint correctly requires admin auth
  • Test every user-facing update endpoint (PATCH /api/profile, PUT /api/account) for the same vulnerability

// API Testing Ideas

  • Authenticate as a standard user; capture the bearer token
  • Send GET /api/users/me; record the role field โ€” confirm it is 'viewer' (not 'admin')
  • Send PATCH /api/users/me with body { "role": "admin" } and the bearer token
  • Assert the response status is 200 but the role in the response body is still 'viewer' โ€” not 'admin'
  • Send GET /api/users/me again and assert role is still 'viewer'
  • If role is 'admin' in either response, the mass assignment bug is confirmed

// Automation Idea

Authenticate as a standard user. Send PATCH /api/users/me with { "role": "admin" }. Immediately send GET /api/users/me and assert the role field is still 'viewer' (or whatever the user's original role was). If the GET returns role: 'admin', fail the test and report the escalation. Extend the test to other privileged fields: isAdmin, plan, credits.

// Expected Result

PATCH /api/users/me ignores or rejects any attempt to update the role field. The response returns the unchanged role, and a subsequent GET /api/users/me confirms the role was not modified.

// Actual Result (Example)

PATCH /api/users/me with body { "role": "admin" } returns 200 OK with the response body showing role: 'admin'. A subsequent GET /api/users/me confirms the role was persisted as 'admin'. The user now has admin-level access to all protected endpoints.

// REPORT IT

Example Bug Report

Title
Standard user can escalate their own role to admin via PATCH /api/users/me
Severity
Critical
Environment
Staging environment Postman Standard user account (initial role: viewer) Bearer token from authenticated request
Steps to Reproduce
  1. 01Log in as a standard user and copy the bearer token from DevTools
  2. 02Send GET /api/users/me with the bearer token; confirm role is 'viewer'
  3. 03Send PATCH /api/users/me with body { "role": "admin" } and the bearer token
  4. 04Read the role field in the response body
  5. 05Send GET /api/users/me again and read the role field
Expected Result
The role field remains 'viewer' in both the PATCH response and the subsequent GET.
Actual Result
The PATCH response returns role: 'admin'. GET /api/users/me confirms role: 'admin'. The user can now access admin-only endpoints.
Impact
Any authenticated user can permanently escalate their own account to admin, gaining unrestricted access to all protected data and actions across the application.

// RELATED