Mass Assignment in APIs: Prevent Unsafe Object Binding
Learn how mass assignment lets clients modify unintended object properties, how it maps to OWASP API3:2023, and how explicit schemas and authorization prevent it.
Primary source: authoritative reference
Mass assignment happens when an application maps client-controlled properties directly onto an internal object without restricting which properties the client may change. A profile endpoint may intend to accept display_name but accidentally accept is_admin, account_status, or another server-controlled property as well.
The problem is not limited to one ORM or protocol. OWASP also calls it autobinding or object injection, depending on the ecosystem. In the 2023 OWASP API Security Top 10, unsafe property updates fall under API3:2023 Broken Object Property Level Authorization, not API6.
How the vulnerability appears
Consider an authenticated profile update that should allow only a display name and biography:
PATCH /api/profile
Content-Type: application/json
{
"display_name": "River",
"bio": "Application developer"
}
An attacker can add properties that the user interface never sends:
PATCH /api/profile
Content-Type: application/json
{
"display_name": "River",
"bio": "Application developer",
"is_admin": true
}
If the handler passes that body to a generic model update and the model accepts is_admin, the endpoint may persist an unauthorized change. Hiding a field in the user interface does not protect it; the server must enforce property-level authorization.
Mass assignment is distinct from object-level authorization. An endpoint might correctly ensure that a user can edit only their own profile yet still let that user change forbidden properties on that profile. Both checks are required.
The primary defense: an explicit write schema
Define a request object, serializer, form, or data transfer object containing only properties that the endpoint permits. Map the validated object to the domain model instead of passing the raw request body through.
type ProfileUpdate = {
displayName?: string;
bio?: string;
};
const update: ProfileUpdate = {
displayName: parsed.display_name,
bio: parsed.bio,
};
await profiles.updateForUser(authenticatedUser.id, update);
The exact syntax varies, but the boundary should remain explicit:
- reject or ignore unknown properties according to a documented API contract;
- allowlist writable properties for each operation, rather than for an entire model;
- use separate input types for registration, self-service profile changes, and administrative updates;
- apply the same rules recursively to nested objects and arrays;
- authorize the requested action and property changes on the server;
- keep database constraints and invariant checks as additional safeguards.
Validation alone is not authorization. A Boolean is_admin value can be perfectly valid data while still being forbidden for a self-service endpoint.
Framework safeguards still need deliberate configuration
Major frameworks provide controls, but their behavior and bypass methods differ. Follow the documentation for the version in production.
Ruby on Rails
Rails Strong Parameters prevents controller parameters from being used for Active Model mass assignment until properties are explicitly permitted. Keep the permitted set scoped to the operation; do not reuse an administrative parameter set in a user-facing action. See the official Action Controller guide.
Django
Django recommends explicitly listing editable ModelForm fields. Its documentation warns that automatically including fields can create security problems when a model later gains a new property. The same allowlist principle applies to API serializers and manual update logic. See Selecting the fields to use.
Laravel Eloquent
Eloquent protects models against mass assignment by default through $fillable or $guarded. Laravel warns that unguarding a model requires hand-crafted arrays and that methods such as forceFill bypass normal protection. Prefer validated input plus a narrow $fillable list. See Laravel's mass assignment documentation.
Framework protection is a layer, not a substitute for an endpoint-specific contract. A broadly fillable model can still be unsafe in a route with narrower authorization requirements.
Review and test safely
Test only systems you own or are explicitly authorized to assess. For each create, update, and patch endpoint:
- Identify the properties the operation is intended to accept.
- Review every place the handler copies, spreads, binds, fills, or assigns client input.
- Check nested objects, merge-patch behavior, GraphQL inputs, and alternate content types.
- Add a negative test containing a known server-controlled property.
- Verify both the response and persisted state; a response alone may not show a side effect.
- Confirm that role changes, ownership changes, billing fields, approval state, and security settings require their own authorization paths.
An example regression test can assert that an authenticated user cannot modify an administrative property:
it("rejects server-controlled profile properties", async () => {
const response = await updateOwnProfile({
display_name: "River",
is_admin: true,
});
expect(response.status).toBe(400);
expect(await readCurrentUser()).toMatchObject({ is_admin: false });
});
A 200 OK response to an unknown property is not conclusive if the API intentionally ignores unknown input. The security requirement is that the forbidden property cannot change. If the contract rejects unknown fields, test the rejection as well.
Incident response when a property was writable
If a production endpoint accepted unauthorized properties:
- restrict or disable the affected write path;
- add the explicit property allowlist and authorization checks;
- identify which records and properties may have changed;
- review trustworthy audit logs for affected accounts and time ranges;
- restore unauthorized changes through a controlled process;
- rotate credentials or invalidate sessions only when the affected properties or evidence warrant it;
- add regression tests for the vulnerable request and nearby endpoints.
Do not assume the issue is resolved after adding a client-side field filter. Requests can bypass the client, so the enforcement belongs at the server boundary.
Primary sources
- OWASP API3:2023 Broken Object Property Level Authorization
- OWASP Mass Assignment Cheat Sheet
- Ruby on Rails Action Controller: Strong Parameters
- Django: Selecting
ModelFormfields - Laravel Eloquent: Mass Assignment
Inspect API JSON Locally
Format and inspect a JSON request or response in your browser before reviewing its property contract. OpsecForge does not send the pasted JSON to its server.
Open JSON Formatter →