Mass Assignment

ID

mass_assignment

Severity

high (privilege fields) / low (identity / financial fields)

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Family

API3:2023 - Broken Object Property Level Authorization

CWE

CWE-915

Resource

data_exposure

Language

any

Description

Walks each endpoint’s request body and parameter list for protected-attribute field names that the caller shouldn’t be allowed to set — typically privilege fields (admin, role, is_admin, is_staff, scopes, permissions) and identity / financial fields (owner, userId, balance, verified).

Severity is tiered:

  • HIGH — privilege fields (the GitHub-2012 / Rails mass-assignment class of vulnerability — write is_admin=true in the signup form).

  • LOW — identity / financial fields (advisory; some legitimate flows write these).

Rationale

Modern web frameworks bind incoming JSON bodies directly to ORM entities (Rails attributes=, Spring @RequestBody MyEntity, Django Model(**request.data), ASP.NET model binding, Laravel fill($request→all())). The mechanism is ergonomic — and dangerous when the entity has fields the request was never supposed to set.

The canonical pattern is the signup endpoint that accepts {username, password, email} but is implemented by binding to a User entity that also has an isAdmin boolean. A caller who adds "isAdmin": true to the JSON gets an admin account at sign-up time — no further exploit needed. The 2012 GitHub Rails breach was exactly this pattern. It still re-appears in modern code regularly.

Identity and financial fields are the lower-tier variant: write userId and the new record is owned by someone else; write balance and the value is whatever the caller specified. These are LOW because legitimate flows sometimes write them (admin tooling, internal mutators) — but the same field being settable from an end-user endpoint is a finding.

Remediation

The fix is to never bind a request body directly to a persistence model. Use an explicit allowlist:

  • Spring: define a dedicated request DTO (record / class) with only the fields the operation accepts. @RequestBody CreateUserRequest req, then map to the entity in the service layer. Do not annotate the entity itself with @RequestBody.

  • Django / DRF: declare fields = ('username', 'email') (not all) on the Serializer.Meta.

  • FastAPI: use a Pydantic model whose fields are exactly the accepted inputs. Pydantic ignores unknown fields by default; set model_config = ConfigDict(extra='forbid') to reject them outright.

  • Rails: params.require(:user).permit(:username, :email) — strong parameters.

  • Laravel: $request→validated() + a FormRequest with explicit rules, or $request→only(['username', 'email']).

  • Express + Mongoose: a Joi / Zod / yup schema in front of the route; Model.create(validated).

If the ORM auto-binds the full body and you cannot stop that, strip the protected fields server-side before persisting (req.body.isAdmin = undefined), and add a database-level constraint (CHECK (is_admin = false) for the user-signup path) as a safety net.

Configuration

The detector ships with a built-in list of privilege fields (HIGH-tier) and identity / financial fields (LOW-tier). Both lists are extensible via the detector configuration — add project-specific protected attributes if your domain has them (isSuperuser, creditLimit, tenantId, …).

Unless your project has bespoke privilege fields, you typically do not need to configure this detector.

References