Choose your stack

Node.js · Express

Fix mass assignment (over-posting) in Node.js and Express

Never pass req.body to Object.assign, a spread or an ORM create or update on a stored record.

Open the fix card
Python · Flask

Fix mass assignment (over-posting) in Python and Flask

Never copy request.get_json() onto a record with dict.update(), a setattr loop or Model(**data).

Open the fix card
Java · Spring Boot

Fix mass assignment (over-posting) in Java and Spring Boot

Bind @RequestBody to a record that declares only the fields users may edit (here ProfileUpdate(displayName, bio)), never to the entity, and copy those fields onto the entity yourself.

Open the fix card
C# · ASP.NET Core

Fix mass assignment (over-posting) in C# and ASP.NET Core

Bind the body to an input model record that declares only the fields users may edit (here ProfileUpdate(DisplayName, Bio)), never to the entity, and copy those fields onto the entity yourself.

Open the fix card
TypeScript · Next.js

Fix mass assignment in TypeScript and Next.js

Never pass await request.json() or Object.fromEntries(formData) to Object.assign, a spread or an ORM create or update.

Open the fix card
Python · FastAPI

Fix mass assignment in Python and FastAPI

Give each write route its own Pydantic body model that declares only the fields that caller may change (here displayName and bio), never the model that describes the whole stored user, and copy model_dump(exclude_none=True) onto the record. extra="forbid" turns any other key into a 422.

Open the fix card
Python · Django

Fix mass assignment in Python and Django

Give every ModelForm an explicit allowlist, fields = ["display_name", "bio"]: it binds only those fields and ignores the rest of the request.

Open the fix card
Go · net/http

Fix mass assignment in Go and net/http

Decode the body into an input struct that declares only the fields users may edit (here ProfileUpdate with DisplayName and Bio), never into the stored struct, call DisallowUnknownFields, and copy each field across yourself. encoding/json matches keys to fields case-insensitively, so a blocklist that refuses "role" by its exact name lets "Role" through.

Open the fix card
Ruby · Rails

Fix mass assignment in Ruby on Rails

Pass only an allowlist to the model: user.update(params.permit(:display_name, :bio)), or params.expect(user: [:display_name, :bio]) for a nested body.

Open the fix card
PHP · Laravel

Fix mass assignment (over-posting) in PHP and Laravel

Give each model an allowlist, #[Fillable(['display_name', 'bio'])] (or $fillable), and pass only validated input: $profile->update($request->validate([...rules for the editable fields...])). validate() returns just the keys it has rules for.

Open the fix card

How it happens

  • The handler gives the request body to code that sets fields by name: Object.assign(user, req.body) or a spread in JavaScript; user.update(data), a setattr loop or Model(**data) in Python; Jackson's readerForUpdating(entity) or @RequestBody bound to the entity in Spring; binding the entity type, or copying a bound object with a mapper, in ASP.NET Core. Every field the client names is written, including ones the form never shows.
  • The entity class doubles as the API model. It has setters for role and tenantId because the admin screen, sign-up and data migrations need them, and the binder cannot tell which caller may use which setter.
  • The fix is a blocklist. After a pentest reports role escalation, the team strips role from the body or marks it read-only, and tenantId, ownerId or any field added later stays writable. Spring's documentation calls disallowedFields fragile for exactly this reason.
  • The check and the binder match names differently. ASP.NET Core's JSON binding matches property names case-insensitively, so a check that refuses role by its exact name lets Role through to the entity. In Node and Python a case variant does not touch role, but a merge stores it as a new field, which a document database keeps.
  • Hidden or disabled form fields, a read-only UI and client-side validation do not limit what a client sends: any HTTP client can add a field to the JSON.

Fix principles

  • Bind each action to its own input type that declares only the fields that caller may change: a zod schema, a Pydantic model, a Java record, a C# record. Never bind request data to entities, ORM models or stored dictionaries.
  • Copy the input type's fields onto the entity explicitly, one assignment per field, so a column added to the entity never becomes writable by accident.
  • Set server-owned fields (role, tenantId, ownerId, verified flags, balances) only in server-side code: an admin endpoint with its own authorization check, sign-up, an invitation flow.
  • Decide what happens to fields the input type does not declare. The Express, Flask, Spring and ASP.NET Core cards ignore them, which is the default in all four libraries. Refusing them with 400 (zod's strictObject, Pydantic's extra='forbid', Jackson's FAIL_ON_UNKNOWN_PROPERTIES, System.Text.Json's JsonUnmappedMemberHandling.Disallow) is stricter and makes probing visible; the FastAPI card does that with extra='forbid', which FastAPI answers with 422. Never pass them through (z.looseObject, extra='allow').
  • Prefer allowlists to blocklists. A blocklist only stops the names you thought of, and only when it compares names the way the binder does.
  • Treat field-level rules as authorization. When an admin may change role and a member may not, give the admin a separate endpoint and input type behind its own check rather than one type with conditional fields.

The scenario

PATCH /me lets a signed-in user edit their own profile. The stored user has displayName and bio, which users may edit, and role and tenantId, which only the server sets. The synthetic fixture has two users: Alice, a member of tenant red, and Bob, an admin of tenant blue. The signed-in user comes from authentication (a test stub), never from the body. Every test sends a plain JSON body as Alice; the exploit tests add role: "admin", tenantId: "blue", or the case variants Role and TenantId to an otherwise ordinary update, then read the stored record.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • Every stack runs the same six tests on the same seed file with byte-identical JSON bodies. The functional tests update displayName and bio, update bio alone, and expect 401 without a signed-in user. The exploit tests send an ordinary update plus role: "admin", plus tenantId: "blue", and plus the case variants Role and TenantId. They accept 200 (extra fields ignored) or 400 (refused), then check the stored record: role must still be member, tenantId red, and in Node and Python, where records are plain objects, no new fields.
  • Static analysis barely covers this. Semgrep has an Express audit rule that follows request data into Object.assign (it flagged this card's fixed code as well as the vulnerable code, and missed the blocklist), an ASP.NET Core MVC rule that only follows bound parameters into View(), and a Django rule; only the MVC rule is in the common packs. CodeQL's one mass assignment query is for Ruby. Each card's scannerRules records what was run and what it found.

Identifiers

CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) lists Mass Assignment and AutoBinding as alternate terms. The OWASP Top 10:2025 maps CWE-915 to A08:2025 Software or Data Integrity Failures, and the OWASP API Security Top 10 2023 folds mass assignment into API3:2023 Broken Object Property Level Authorization. ASVS 5.0.0 asks for it in 15.3.3 (limit the allowed fields per controller and action) and 8.2.3 (field-level access only for consumers with explicit permission). CAPEC is left empty on purpose: MITRE lists no attack pattern for CWE-915. CWE-1321 (prototype pollution) is a child of CWE-915 but a different bug, and these cards do not cover it.

References

  1. OWASP Mass Assignment Cheat Sheet
  2. OWASP API Security Top 10 2023: API3 Broken Object Property Level Authorization
  3. OWASP Top 10:2025 A08 Software or Data Integrity Failures
  4. CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
  5. OWASP ASVS 5.0.0, V15 Secure Coding and Architecture (15.3.3 mass assignment)
  6. OWASP ASVS 5.0.0, V8 Authorization (8.2.3 field-level access)
  7. GitHub (2012): Public key security vulnerability and mitigation (a mass assignment incident)

Practice in the browser

Guides on this weakness