Introduction
Mass assignment is one of the most elegant attack techniques in an API pentester's arsenal. With a single extra parameter in a JSON payload, an attacker can escalate from a regular user to an administrator, modify billing information, or access data they were never meant to see.
What Is Mass Assignment?
Mass assignment occurs when an API endpoint automatically binds client-provided data to internal data models without explicitly filtering which fields are allowed:
// Vulnerable Express.js endpoint
app.put('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
Object.assign(user, req.body); // Blindly assigns ALL fields
await user.save();
res.json(user);
});
The developer expects clients to send:
{ "name": "John", "email": "john@example.com" }
But an attacker sends:
{ "name": "John", "email": "john@example.com", "role": "admin", "billing_plan": "enterprise" }
The Discovery Process
1. Response Analysis
The first step is examining API responses. When a GET endpoint returns fields like role, is_admin, permissions, or plan_tier, those field names become our attack parameters.
2. Documentation Mining
API documentation (Swagger/OpenAPI specs) often reveals model schemas that include internal fields:
User:
properties:
name: { type: string }
email: { type: string }
role: { type: string, enum: [user, admin, superadmin] }
org_id: { type: integer }
credit_balance: { type: number }
3. Error-Based Discovery
Sending invalid types for guessed parameters can confirm their existence:
PUT /api/users/me
{ "role": 12345 }
Response: { "error": "role must be a string, expected one of: user, admin" }
4. Framework-Specific Techniques
Different frameworks have different default behaviors:
- Ruby on Rails: Strong Parameters were introduced to prevent mass assignment, but developers often permit too many fields with
params.permit! - Django REST Framework: Serializers without explicit
fieldsorread_only_fieldsare vulnerable - Spring Boot:
@ModelAttributebinds all request parameters to object fields by default - Laravel: Models without
$fillableor$guardedproperties accept all input
Real-World Exploitation Scenarios
Privilege Escalation
PATCH /api/users/me
{ "role": "admin" }
Cross-Tenant Data Access
PATCH /api/users/me
{ "organization_id": "target-org-uuid" }
Financial Manipulation
PATCH /api/users/me
{ "credit_balance": 999999, "billing_plan": "enterprise" }
Bypassing Verification
PATCH /api/users/me
{ "email_verified": true, "phone_verified": true }
Prevention
- Explicit allowlists: Only permit specific fields for each endpoint
- Separate DTOs: Use Data Transfer Objects that map only intended fields
- Read-only annotations: Mark sensitive fields as read-only in your ORM/serializer
- Integration tests: Write tests that attempt to modify protected fields
- API gateway validation: Enforce request schemas at the gateway level
Conclusion
Mass assignment is the perfect example of why automated scanning falls short. A scanner sees a valid 200 response and moves on. A human pentester understands that "role": "admin" in the response means it might be writable — and tests it. One missing parameter filter can expose your entire infrastructure.