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: false rejects 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.

Leave a Comment