API Security Mass Assignment Attacks
Mass assignment is a vulnerability where an attacker sends extra fields in a request body, and the server blindly processes all of them — including fields that should never be user-editable, like roles, account balances, or admin flags. A single extra JSON key can turn a regular user into an administrator.
How Mass Assignment Works
Many web frameworks offer a convenience feature: automatically map incoming request fields to a model object. This saves developers from writing repetitive code. But when used without a field allowlist, it creates a serious vulnerability.
Normal signup request:
POST /api/users/register
{
"name": "Rohan Verma",
"email": "rohan@example.com",
"password": "SecurePass@123"
}
Attacker's signup request (extra fields added):
POST /api/users/register
{
"name": "Rohan Verma",
"email": "rohan@example.com",
"password": "SecurePass@123",
"role": "admin",
"is_verified": true,
"account_balance": 99999,
"subscription_plan": "enterprise"
}
Vulnerable server code (Node.js + Mongoose):
const user = new User(req.body); ← Maps ALL body fields
await user.save();
Result: Rohan is now an admin, pre-verified, with ₹99,999 balance
and an enterprise subscription — all from a registration request.
Real-World Mass Assignment Incidents
GitHub (2012): Rails developer Egor Homakov exploited mass assignment in GitHub's Ruby on Rails application. By adding "public_key[user_id]" to a form submission, he added his SSH key to the Rails core repository without permission. GitHub had not restricted which model attributes could be mass-assigned. This incident led Rails to change its default behavior to require explicit attribute allowlisting. HackerOne Platform Bug: Security researchers found mass assignment vulnerabilities in bug bounty platform APIs where adding privilege-level fields to profile update requests elevated account permissions. Multiple bounties were paid for this class of vulnerability.
Vulnerable Code Patterns Across Languages
Ruby on Rails (vulnerable — before strong parameters):
def create
@user = User.new(params[:user]) ← All params mapped
@user.save
end
Ruby on Rails (fixed — strong parameters):
def user_params
params.require(:user).permit(:name, :email, :password)
end
def create
@user = User.new(user_params) ← Only permitted fields
@user.save
end
Python/Django (vulnerable):
user = User(**request.data) ← Maps everything
user.save()
Python/Django (fixed):
allowed = {k: request.data[k] for k in ['name', 'email', 'password']
if k in request.data}
user = User(**allowed)
user.save()
Node.js/Express (vulnerable):
const user = new User(req.body) ← Maps all body fields
Node.js/Express (fixed — explicit field picking):
const user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
// role, is_admin, balance NOT here — cannot be set by user
});
Java/Spring (vulnerable):
@PostMapping("/users")
public User create(@RequestBody User user) {
return userRepository.save(user); ← Binds all JSON fields to User
}
Java/Spring (fixed — use DTO):
@PostMapping("/users")
public User create(@RequestBody CreateUserDTO dto) {
User user = new User();
user.setName(dto.getName());
user.setEmail(dto.getEmail());
// role, adminFlag fields not in CreateUserDTO — cannot be set
return userRepository.save(user);
}
Fields Commonly Targeted in Mass Assignment Attacks
High-Value Target Fields: Access Control: role, user_role, account_type, is_admin, is_superuser permissions, scopes, access_level, privilege_level Financial: balance, credit, account_balance, wallet_amount discount_rate, price_override, subscription_tier coins, points, tokens (virtual currency) Verification and Trust: is_verified, email_verified, phone_verified is_trusted, kyc_status, identity_verified Account Status: is_active, is_banned, is_suspended, account_locked subscription_plan, plan_expiry, trial_active Internal Tracking: internal_score, fraud_flag, risk_level created_by, created_at (tamper with audit trail) owner_id, company_id (change resource ownership)
Prevention Strategy
Strategy 1: Use DTOs (Data Transfer Objects)
A DTO is a separate class that defines exactly which fields are accepted from the client. The server maps the DTO to the internal model — only the fields defined in the DTO can ever be set from user input.
DTO Pattern Example:
// What the client can send:
class RegisterUserDTO {
name: string;
email: string;
password: string;
// role is NOT here — client cannot set it
}
// Internal model (not exposed to client):
class User {
id: string;
name: string;
email: string;
passwordHash: string;
role: string; // Set by server only (default: 'user')
isAdmin: boolean; // Set by server only (default: false)
balance: number; // Set by server only (default: 0)
}
// Mapping (only DTO fields ever touch the model):
const user = new User();
user.name = dto.name;
user.email = dto.email;
user.role = 'user'; // Hardcoded server default
Strategy 2: Schema Validation with additionalProperties: false
JSON Schema validation can reject unknown fields entirely:
{
"type": "object",
"additionalProperties": false, ← Reject any field not listed below
"required": ["name", "email", "password"],
"properties": {
"name": { "type": "string", "maxLength": 100 },
"email": { "type": "string", "format": "email" },
"password": { "type": "string", "minLength": 8 }
}
}
Request with extra fields:
{ "name": "Rohan", "email": "...", "password": "...", "role": "admin" }
→ Schema validation fails: "role" is an additional property
→ 400 Bad Request returned before any processing
Strategy 3: Separate Endpoints for Privileged Fields
Create separate endpoints for actions requiring different authorization:
Public endpoint (any authenticated user):
PATCH /api/users/profile
Allowed fields: name, avatar_url, bio, notification_preferences
Admin-only endpoint (admin role required):
PATCH /api/admin/users/{id}/role
Allowed fields: role, is_active
Admin-only endpoint (admin role required):
PATCH /api/admin/users/{id}/balance
Allowed fields: balance_adjustment, reason
Each endpoint handles a specific set of fields.
Privilege elevation requires hitting a different endpoint
that requires a higher role — not just adding a field to a request.
Testing for Mass Assignment
Manual Testing Steps:
1. Identify an update endpoint: PATCH /api/users/profile
2. Send the normal request and note which fields are accepted:
{ "name": "New Name" } → 200 OK
3. Add sensitive fields one at a time and observe server behavior:
{ "name": "New Name", "role": "admin" } → 200 or 400?
{ "name": "New Name", "is_admin": true } → 200 or 400?
{ "name": "New Name", "balance": 999999 } → 200 or 400?
{ "name": "New Name", "email_verified": true } → 200 or 400?
4. After each test, check whether the field was actually applied:
GET /api/users/profile → Did role change to admin?
5. Any field that is silently accepted AND applied = mass assignment vulnerability.
Automated: Tools like Burp Suite Intruder can fuzz all fields
from the data model schema against update endpoints.
Key Points
- Mass assignment allows attackers to set server-side fields like role, balance, and admin status by simply including them in a request body.
- The vulnerability comes from mapping all request fields to model objects without filtering.
- Use DTOs that explicitly define only client-editable fields — never bind request bodies directly to data models.
- JSON Schema with
additionalProperties: falserejects any unknown field at the validation layer. - Privileged field changes (role, balance, subscription) must be on separate endpoints requiring elevated authorization — not settable via regular profile update endpoints.
