Improperly Controlled Modification of Dynamically-Determined Object Attributes

Description

Improperly Controlled Modification of Dynamically-Determined Object Attributes (also known as Mass Assignment, Autobinding, or Object Injection) occurs when software receives input that specifies multiple object attributes for initialization or update, but fails to properly restrict which attributes can be modified. When an application blindly assigns all incoming parameters to an object's attributes, attackers can modify internal or sensitive attributes that should not be externally accessible, such as privilege levels, account balances, or administrative flags.

Risk

This vulnerability can lead to severe security breaches. Attackers can escalate privileges by modifying role or permission attributes. Financial systems may be compromised by manipulating balance or transaction fields. Authentication can be bypassed by modifying verification flags. In JavaScript environments, prototype pollution attacks can affect all objects in the application. The risk is amplified because developers often assume certain attributes are "internal" and protected, when in reality the binding mechanism exposes them to modification. This vulnerability is particularly common in web frameworks that provide automatic parameter binding.

Solution

Implement explicit attribute whitelisting that specifies exactly which attributes can be modified through external input. Use framework-provided protection mechanisms (strong parameters in Rails, @JsonIgnore in Java, etc.). Separate DTOs (Data Transfer Objects) from domain models to control what can be bound. Never directly bind request parameters to internal domain objects. For JavaScript, filter out dangerous keys like __proto__, constructor, and prototype. Implement input validation that rejects unexpected attributes. Consider using immutable objects where appropriate.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - Attackers can alter sensitive data or program variables that should be protected.
IntegrityScope: Integrity

Execute Unauthorized Code or Commands - Through prototype pollution or object manipulation, attackers may alter execution logic.
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Modifying role or permission attributes enables privilege escalation.

Example Code

Vulnerable Code

# Vulnerable: Mass assignment in Rails (pre-Rails 4 style)
class User < ActiveRecord::Base
  # All attributes are mass-assignable by default
end

class UsersController < ApplicationController
  def create
    # Vulnerable: All params directly assigned
    @user = User.new(params[:user])
    @user.save
  end

  def update
    @user = User.find(params[:id])
    # Vulnerable: Can modify any attribute including :admin, :role
    @user.update(params[:user])
  end
end

# Attack: POST /users with params: user[name]=hacker&user[admin]=true
# Vulnerable: Django mass assignment
from django.db import models

class User(models.Model):
    username = models.CharField(max_length=100)
    email = models.EmailField()
    is_admin = models.BooleanField(default=False)
    balance = models.DecimalField(max_digits=10, decimal_places=2)

def vulnerable_update_user(request, user_id):
    user = User.objects.get(id=user_id)

    # Vulnerable: All POST data assigned to model
    for key, value in request.POST.items():
        setattr(user, key, value)

    user.save()

# Attack: POST with is_admin=True or balance=999999
// Vulnerable: Spring autobinding
@Controller
public class VulnerableUserController {

    @PostMapping("/register")
    public String register(User user) {
        // Vulnerable: All request parameters bound to User
        // Attacker can set user.role=ADMIN
        userRepository.save(user);
        return "success";
    }
}

public class User {
    private String username;
    private String password;
    private String role = "USER";  // Can be overwritten!
    private boolean verified = false;  // Can be overwritten!

    // Getters and setters for all fields
}
// Vulnerable: Prototype pollution via mass assignment
function vulnerableUpdateConfig(config, updates) {
    // Vulnerable: Copies all properties without filtering
    Object.assign(config, updates);
}

// Attack:
const config = { theme: 'dark' };
const maliciousUpdates = JSON.parse(
    '{"__proto__": {"polluted": true}}'
);
vulnerableUpdateConfig(config, maliciousUpdates);

// Now ALL objects have .polluted property!
console.log({}.polluted);  // true
// Vulnerable: Path-based object modification
function vulnerableSetByPath(object, path, value) {
    const pathArray = path.split('.');
    let current = object;

    for (let i = 0; i < pathArray.length - 1; i++) {
        // Vulnerable: No filtering of dangerous paths
        current = current[pathArray[i]];
    }

    current[pathArray[pathArray.length - 1]] = value;
}

// Attack:
const obj = {};
vulnerableSetByPath(obj, '__proto__.polluted', true);
console.log({}.polluted);  // true - prototype polluted!
// Vulnerable: PHP object injection via unserialize
<?php
class User {
    public $username;
    public $role = 'user';
    public $isAdmin = false;
}

// Vulnerable: Deserializing user input
$userData = unserialize($_COOKIE['user_data']);

// Attacker crafts serialized string:
// O:4:"User":3:{s:8:"username";s:6:"hacker";s:4:"role";s:5:"admin";s:7:"isAdmin";b:1;}
// This creates User with role=admin, isAdmin=true
?>

Fixed Code

# Fixed: Strong parameters in Rails 4+
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    @user.save
  end

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

  private

  # Fixed: Explicit whitelist of allowed parameters
  def user_params
    params.require(:user).permit(:name, :email, :password)
    # :admin, :role, :balance are NOT permitted
  end
end
# Fixed: Explicit attribute whitelist
from django.db import models

class User(models.Model):
    username = models.CharField(max_length=100)
    email = models.EmailField()
    is_admin = models.BooleanField(default=False)
    balance = models.DecimalField(max_digits=10, decimal_places=2)

    # Define which fields can be updated via API
    ALLOWED_UPDATE_FIELDS = {'username', 'email'}

def fixed_update_user(request, user_id):
    user = User.objects.get(id=user_id)

    # Fixed: Only update allowed fields
    for key, value in request.POST.items():
        if key in User.ALLOWED_UPDATE_FIELDS:
            setattr(user, key, value)
        else:
            # Log attempted modification of forbidden field
            logger.warning(f"Rejected field update: {key}")

    user.save()

# Better: Use serializers (Django REST Framework)
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username', 'email']  # Only these are assignable
        read_only_fields = ['is_admin', 'balance']  # Never assignable
// Fixed: Use DTO with explicit fields
@Controller
public class FixedUserController {

    @PostMapping("/register")
    public String register(@Valid UserRegistrationDTO dto) {
        // Fixed: Only DTO fields are bound
        User user = new User();
        user.setUsername(dto.getUsername());
        user.setPassword(passwordEncoder.encode(dto.getPassword()));
        user.setRole("USER");  // Set internally, not from input
        user.setVerified(false);  // Set internally

        userRepository.save(user);
        return "success";
    }
}

// DTO only contains allowed fields
public class UserRegistrationDTO {
    @NotBlank
    private String username;

    @NotBlank
    private String password;

    // No role, no verified - these cannot be set by user
}

// Or use @JsonIgnore on sensitive fields
public class User {
    private String username;
    private String password;

    @JsonIgnore  // Never bound from JSON
    private String role = "USER";

    @JsonIgnore
    private boolean verified = false;
}
// Fixed: Filter dangerous properties
function fixedUpdateConfig(config, updates) {
    const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype'];

    // Fixed: Filter out dangerous keys
    const safeUpdates = Object.fromEntries(
        Object.entries(updates).filter(([key]) =>
            !FORBIDDEN_KEYS.includes(key)
        )
    );

    Object.assign(config, safeUpdates);
}

// Better: Whitelist allowed keys
function saferUpdateConfig(config, updates) {
    const ALLOWED_KEYS = ['theme', 'language', 'timezone'];

    const safeUpdates = Object.fromEntries(
        Object.entries(updates).filter(([key]) =>
            ALLOWED_KEYS.includes(key)
        )
    );

    Object.assign(config, safeUpdates);
}
// Fixed: Safe path-based modification
function fixedSetByPath(object, path, value) {
    const FORBIDDEN_SEGMENTS = ['__proto__', 'constructor', 'prototype'];

    const pathArray = path.split('.');

    // Fixed: Validate path segments
    if (pathArray.some(segment => FORBIDDEN_SEGMENTS.includes(segment))) {
        throw new Error(`Forbidden path segment in: ${path}`);
    }

    let current = object;

    for (let i = 0; i < pathArray.length - 1; i++) {
        const segment = pathArray[i];

        // Create intermediate objects if needed
        if (!(segment in current)) {
            current[segment] = {};
        }

        current = current[segment];
    }

    current[pathArray[pathArray.length - 1]] = value;
}

// Even safer: Use a library like lodash with prototype pollution fix
const _ = require('lodash');
// Modern lodash versions protect against prototype pollution
// Fixed: Explicit property assignment
<?php
class User {
    public $username;
    private $role = 'user';
    private $isAdmin = false;

    public function setUsername($username) {
        $this->username = $username;
    }

    // No public setter for role or isAdmin
    // They can only be set through trusted internal methods
    public function promoteToAdmin() {
        // This would require proper authorization check
        $this->role = 'admin';
        $this->isAdmin = true;
    }
}

// Fixed: Never unserialize user input
// Use JSON instead
$userData = json_decode($_COOKIE['user_data'], true);

$user = new User();
// Only set allowed fields
if (isset($userData['username'])) {
    $user->setUsername($userData['username']);
}
// role and isAdmin cannot be set from external input
?>

CVE Examples

  • CVE-2024-3283: LLM application allowed modification of sensitive variables through mass assignment.
  • CVE-2012-2054: Mass assignment vulnerability in web application allowed privilege escalation.
  • CVE-2012-2055: Mass assignment via URL parameters in version control system.
  • CVE-2008-7310: E-commerce application allowed payment bypass through mass assignment.
  • CVE-2013-1465: PHP unserialize vulnerability enabled object injection attack.

  • CWE-913: Improper Control of Dynamically-Managed Code Resources (parent)
  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (child)
  • CWE-502: Deserialization of Untrusted Data (related)

References

  1. MITRE Corporation. "CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes." https://cwe.mitre.org/data/definitions/915.html
  2. OWASP. "Mass Assignment Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html
  3. Ruby on Rails Security Guide. "Mass Assignment."