Mass assignment: how it works, how to test it and how to fix it

Mass assignment is a flaw where an app copies request fields straight into an object, so a caller can set properties they should not control, such as role, price or owner. OWASP now files it under API3:2023 Broken Object Property Level Authorization, and CWE-915 covers it. Fix it with an allow-list of writable fields or a DTO per operation.

Mass assignment is a flaw where an application copies the fields of a request straight into an object, so the caller can set properties the application never meant to expose. One extra field such as role or is_admin in a JSON body can change a protected property. CWE-915 names it "Improperly Controlled Modification of Dynamically-Determined Object Attributes" and lists "Mass Assignment" and "AutoBinding" as other names.

Frameworks call it different things. OWASP's cheat sheet lists mass assignment for Ruby on Rails and Node.js, autobinding for Spring MVC and ASP.NET MVC, and object injection for PHP. OWASP's API list merged it into API3:2023, Broken Object Property Level Authorization.

How does mass assignment work?#

Many frameworks bind request data to a model with one call. If the model has more properties than the form or client uses, a caller who knows or guesses a property name can set it. The server has no rule that says this caller may not write that field.

Illustrative mass assignment: Harbor Notes

This is a demonstration. No real customer system was tested. Harbor Notes is a fictional app and example.com is a reserved example domain.

The profile page sends this request for test user A:

PATCH /api/v1/users/me HTTP/1.1
Host: api.harbor-notes.example.com
Authorization: Bearer <token-of-test-user-A>
Content-Type: application/json

{ "display_name": "Ana" }

The response lists the stored record:

{ "id": 1042, "display_name": "Ana", "role": "member", "plan": "free", "tenant_id": 17 }

The record has fields the form never sends: role, plan and tenant_id. A tester repeats the call on a test account with one extra field, "plan": "team", and then reads the record back with a second request. A vulnerable server returns plan team on the read-back. A safe server ignores the field or answers 400, and the read-back still shows free.

The flaw needs two facts: the server binds fields it was not told to accept, and no check blocks the write. The first is a design choice in the code. The second is missing authorization at the property level.

Where does mass assignment hide in APIs?#

It hides wherever the server turns client data into a stored object. Testers look at each of these places:

  • PATCH and PUT bodies for profile, settings and account updates.
  • POST bodies that create a record, such as sign-up, invitations and orders.
  • Nested objects, such as {"profile": {"role": "admin"}}, where the top level is filtered but the child is not.
  • GraphQL mutation inputs, where the input type may list more fields than the UI uses.
  • Mobile app and single-page app calls, where the client hides a field the API still accepts.
  • Bulk and import endpoints that take whole objects.

Sensitive fields testers try, always on test accounts and records:

Group Example field names
Privilege role, is_admin, permissions, group
Ownership and tenancy owner_id, user_id, tenant_id, org_id
Money and plan price, discount, plan, credits, balance
State verified, email_verified, approved, status, deleted
Identity and audit id, created_at, updated_by

Candidates come from the API's own output. Response bodies list the properties of each object. OpenAPI specs and GraphQL introspection (if enabled) list more. Client-side JavaScript bundles often contain model fields. Error messages sometimes name a rejected field, which confirms that it exists.

What are the real cases?#

The best-known case is GitHub in 2012. OWASP's cheat sheet states that "In 2012, GitHub was hacked using mass assignment." A user could attach a public key to any organization, which allowed changes to that organization's repositories. The cheat sheet does not give a day or a CVE for it.

A second case is in the framework itself. CVE-2013-0269 was published on 13 February 2013. NVD says the JSON gem for Ruby before 1.5.5, 1.6.x before 1.6.8 and 1.7.x before 1.7.7 allowed remote attackers to cause a denial of service or "bypass the mass assignment protection mechanism" through a crafted JSON document. The weakness is listed as CWE-20. It shows that a protection layer can have its own flaws, so the allow-list belongs in your code too.

Mass assignment, IDOR, BOPLA and parameter pollution: what differs?#

These four are close, and one request can touch more than one.

Term What the attacker changes Example Guide
Mass assignment A property the server binds Add "role":"admin" to a PATCH body This page
BOPLA (API3:2023) Any property-level read or write the user may not have Mass assignment plus excessive data exposure OWASP API Top 10 guide
IDOR or BOLA Which object is reached Change /notes/1042 to /notes/2077 IDOR testing
HTTP parameter pollution Which value a component reads Send id twice with different values PortSwigger parameter pollution lessons

How do you prevent mass assignment?#

Decide per operation which fields a caller may write, and bind only those. OWASP's cheat sheet gives the architectural answer: use Data Transfer Objects (DTOs) so that only fields meant to be editable are in the object you bind. When you must bind to a model, allow-list the permitted fields. Do not rely on a block-list alone. A DTO and an allow-list limit what can be written, and they do not replace an authorization check on the object and the operation.

Method How it works Weakness Use it
Allow-list Name the writable fields Must be kept current Default choice
DTO per operation Separate input type with only editable fields More classes Best for new APIs
Deny-list Name the protected fields A new or missed field stays writable Only as an extra layer

Set sensitive values on the server: owner, tenant, role and plan come from the session or from business logic, never from the body. Reject unknown fields where the framework allows, because a clear 400 helps your tests find gaps.

Fixes by framework#

Each pair shows the risky form and the fixed form.

Ruby on Rails uses strong parameters:

# Risky: binds every submitted attribute
User.update(params[:user])
# Fixed: names the writable attributes
User.update(params.require(:user).permit(:display_name, :avatar_url))

Django REST Framework lists serializer fields and marks others read-only:

# Risky: all model fields are writable
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"
# Fixed: explicit fields, sensitive ones read-only
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "display_name", "role", "plan"]
        read_only_fields = ["id", "role", "plan"]

Spring MVC binds to a DTO and keeps the entity out of the request:

// Risky: the entity has role and plan
@PatchMapping("/users/me") User update(@RequestBody User body) { ... }
// Fixed: a record with only editable fields
record ProfileUpdate(String displayName) {}
@PatchMapping("/users/me") User update(@RequestBody ProfileUpdate body, Principal p) { ... }

ASP.NET Core uses a view model, or [BindNever] on protected properties:

// Risky: binds every public property of User
public IActionResult Update(User user) { ... }
// Fixed: a DTO, plus BindNever as a second layer on the entity
public record ProfileUpdate(string DisplayName);
public IActionResult Update(ProfileUpdate input) { ... }
public class User { [BindNever] public string Role { get; set; } }

NestJS validates a DTO with class-validator and drops unknown fields:

// main.ts: strip unlisted fields and reject extras
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
// DTO
export class UpdateProfileDto { @IsString() displayName: string; }

Express with Mongoose copies picked fields, as OWASP shows with _.pick:

// Risky: User.findByIdAndUpdate(id, req.body)
// Fixed:
const update = _.pick(req.body, ["displayName", "avatarUrl"]);
await User.findByIdAndUpdate(req.user.id, update);

Laravel Eloquent uses $fillable:

class User extends Model {
    protected $fillable = ['display_name', 'avatar_url']; // allow-list
}

Go decodes JSON into a struct that has only the writable fields:

type ProfileUpdate struct {
    DisplayName string `json:"display_name"`
}
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // returns an error on role, plan, tenant_id
var in ProfileUpdate
if err := dec.Decode(&in); err != nil { http.Error(w, "bad request", 400); return }

How do penetration testers detect and prove mass assignment?#

Proof is a change that persists and that a second session can read. A 200 response that echoes the field is not proof, because many APIs echo input without storing it. Our method requires a read-back, and controls run beside the test.

  1. Create two test accounts, A and B, in the same tenant, and note A's role and plan.
  2. Collect candidate fields from responses, specs, error messages and JavaScript bundles.
  3. Run a control: send an unknown field name such as zz_test and a field the user may legitimately write. Record how the server treats each.
  4. Add one candidate field per request on a test-owned record. Change one thing at a time.
  5. Read back through GET as the same user, then as another session (account B or an admin test account) to see the effect from outside.
  6. Check the effect: a changed role or plan must change what the account can do. State what the changed field allowed.
  7. Restore the record and note this in the report.

Sample evidence note

Demonstration only. Harbor Notes is fictional. Control: PATCH with display_name changes the name; PATCH with zz_test returns 200 and the read-back shows no zz_test. Test: PATCH with plan set to team returns 200. Read-back: GET /api/v1/users/me as test user A shows plan team, and the billing page of the admin test account shows the plan change. Result: plan is writable by the account owner. Severity: High on the four-level scale.

Destructive proofs use only test-owned accounts and records, and testing needs written authorization.

Mass assignment testing checklist#

  • List each create and update endpoint and compare the fields returned with the fields the client sends.
  • Try one sensitive field at a time on a test record: role, owner_id, tenant_id, plan, price, verified.
  • Test nested objects, arrays, GraphQL inputs and bulk or import endpoints.
  • Run unknown-field and wrong-type controls before judging a result.
  • Read back as the same user and as a second user or role.
  • Compare PUT, PATCH and POST, because they often bind differently.
  • Check that tenant and owner values come from the session, not from the body.
  • Confirm that a fix returns 400 or ignores the field, and that legitimate edits still work.

When should you get a professional test?#

Get one when your API accepts writes on objects that carry roles, plans, prices or tenant IDs, or after you add endpoints that bind models in bulk. A tester needs accounts for each role and a second tenant, so cross-account effects can be read back. Our API penetration testing covers mass assignment and IDOR in scope, and the JWT guide covers the token side of role claims. Testing starts only after written authorization from the system owner.

Questions

What is the difference between mass assignment and IDOR?

IDOR is about which object you can reach: you change an ID and read someone else's record. Mass assignment is about which properties of an object you can write: you add a field the API did not mean to accept. One request can show both flaws, so testers check each separately.

Is mass assignment the same as BOPLA?

BOPLA, Broken Object Property Level Authorization, is OWASP API3:2023. It merges the 2019 items Excessive Data Exposure and Mass Assignment. Mass assignment is the write half: a user changes a property they should not. Excessive data exposure is the read half.

Is a deny-list enough to stop mass assignment?

No. OWASP's cheat sheet says not to rely on a block-list as the only protection. A field that is added later or missed stays writable. Use an allow-list or a DTO, and keep the deny-list as a second layer if you want one.

How is mass assignment different from HTTP parameter pollution?

Mass assignment adds a property the server binds to an object. HTTP parameter pollution sends the same parameter more than once, or in several places, so that two components read different values. They can combine, but the fixes differ.

Can a scanner find mass assignment?

Rarely. A scanner does not know which fields are sensitive. A tester compares what the API returns with what it accepts, adds a candidate field and reads the record back from a second session.

Sources

  1. OWASP Mass Assignment Cheat Sheet (checked 9 November 2026)
  2. OWASP API Security Top 10 2023: API3 Broken Object Property Level Authorization (checked 9 November 2026)
  3. CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes (checked 9 November 2026)
  4. NVD: CVE-2013-0269 (JSON gem for Ruby) (checked 9 November 2026)
  5. PortSwigger Web Security Academy: server-side parameter pollution and API testing (checked 9 November 2026)