Skip to content

CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes

Overview

This weakness occurs when user input decides which attributes of an object get modified, letting an attacker set properties the code never meant to expose.

The weakness arrives under several names, depending on the framework that produced it: mass assignment in Rails, auto-binding in Spring MVC and ASP.NET MVC, and PHP object injection - though that last term is also used for CWE-502, which is a different weakness. MITRE lists all three as alternate terms for this entry.

Relationship to Other CWEs

CWE-915 is a Base-level entry and MITRE's mapping guidance for it is Allowed, so it is the right number for a mass-assignment finding rather than one to route away from.

It sits under the Class CWE-913 (Improper Control of Dynamically-Managed Code Resources), which has no page here - but CWE-915 is not the fallback for that class. CWE-913 is marked Allowed-with-Review, with MITRE's comment "examine children of this entry to see if there is a better fit", and it has five children in the research view (view-1000), four of which have pages here. Pick by what the attacker controls:

If the input controls File it as
Which attributes of an existing object get set CWE-915 (this page)
Which class or code gets loaded or invoked CWE-470 (Unsafe Reflection)
The code that gets generated or evaluated CWE-94 (Code Injection)
A serialized payload that is reconstructed CWE-502 (Deserialization of Untrusted Data)
Which variable is read or written by name CWE-914 - no page here

The simplified-mapping view (view-1003) lists a different set for CWE-913 - CWE-470, CWE-502 and CWE-1321 - so a scanner following that view will not offer CWE-915 or CWE-94 under this class at all. Check which view a finding's hierarchy came from before re-triaging it.

Three neighbours decide where a finding belongs:

  • CWE-1321 (Prototype Pollution) - CWE-915's child, the JavaScript variant, where the attribute being set is on Object.prototype and the blast radius is every object in the process rather than one record. File a prototype-pollution finding there and follow that page: the allowlisting guidance below still applies, but the JavaScript fix also has to deny __proto__, constructor and prototype as keys, which an allowlist of permitted attributes does by construction and a denylist of forbidden ones usually does not.
  • CWE-1174 (ASP.NET Misconfiguration: Improper Model Validation) - the other half of the same binding step, not a competing number. CWE-915 controls which properties may be set at all; CWE-1174 controls whether the values of the properties you did expose are validated. In ASP.NET they fail together, and fixing either alone leaves a real weakness standing.
  • CWE-502 (Deserialization of Untrusted Data) - a peer that is easy to file this against by mistake, because both end with attacker-influenced object state. MITRE's own relationship note says the boundary needs further exploration and that "CWE-915 is more narrowly scoped to object modification, and is not necessarily used for deserialization". The practical test: if the attacker controls which fields of an existing, expected type get set, it is CWE-915; if they control what type is constructed or reach code during the reconstruction itself, it is CWE-502.

OWASP Classification

A08:2025 - Software or Data Integrity Failures

Risk

High: Attackers can escalate privilege or overwrite fields such as price and balance by setting object attributes the endpoint never meant to expose.

Remediation Steps

Core Principle: Never allow mass assignment of object attributes; bind/allowlist permitted fields and enforce invariants server-side.

Locate the dynamically-determined object attribute modification

  • Review the flaw details for the file, line number, and code pattern
  • Trace the flow from source to sink: which user input decides the attribute name
  • Sources: HTTP parameters, JSON input, form data, external configuration
  • Sinks: setattr(), __dict__, obj[user_input] =, Object.defineProperty(), framework mass assignment
  • Work out what is reachable: can the caller set a security-critical attribute such as isAdmin, role, price or balance?

Restrict attribute modification to allowed properties (Primary Defense)

Name the attributes a caller may set, and refuse everything else:

  • Keep an allowlist of permitted attributes: ALLOWED_ATTRS = {'name', 'email', 'phone'}
  • Check the name against it before assigning: if attr_name not in ALLOWED_ATTRS: raise ValueError
  • Never pass user input through as the attribute name: setattr(obj, user_input, value)

An attacker who reaches this code can then only touch attributes you chose to expose.

Validate the values of the attributes you did allow

The allowlist decides which attributes a caller may set; it says nothing about what they may set them to. An attribute can be on the list and still be assigned a value the object should never hold.

  • Check that the value matches the expected type: string, int, boolean
  • Apply length and format rules: limit string length, validate email and phone formats
  • Re-run the framework's own validation. Dynamic assignment usually writes past the validators a form, serializer, or entity mapping would have run, so call them explicitly before saving
  • Check who may set the attribute, not only whether it is settable: an allowlisted role that any authenticated caller can write is still privilege escalation

Denylisting attribute names - blocking __class__, __dict__, constructor, prototype, is_admin - is not a substitute for the allowlist above, and adding one alongside it buys nothing. See Common Pitfalls.

Use safe APIs for object manipulation (Defense in Depth)

  • Prefer explicit setters and @property accessors - obj.setName(), obj.setEmail() - over dynamic assignment
  • Make objects immutable where the design allows it: frozen dataclasses, namedtuples, read-only properties
  • Avoid dynamic attribute writes driven by untrusted input: setattr, __dict__ updates, reflection-based property setters, and obj[key] = value over a user-supplied key
  • Use the framework's own binding controls: ORM allowlists such as Django fields or Rails permit, and schema validation

Monitor and audit attribute changes

  • Log dynamic attribute modifications with attribute name, value, and source
  • Alert on attempts to modify security-critical attributes such as isAdmin, role, permissions and price
  • Track modification attempts the allowlist rejected
  • Watch for bulk or rapid attribute changes

Test the attribute control fix

  • Test with allowed attributes (should work: name, email, phone)
  • Test with forbidden attributes (isAdmin, role, price, __class__): the request should succeed with the extra attribute silently dropped, or be rejected outright - what it must not do is persist the value
  • Read the stored record back rather than trusting the response: an endpoint that echoes the object it was sent will report the attacker's isAdmin whether or not it saved it
  • Test every endpoint that binds the same object, not only the one in the finding - create, update, PATCH, bulk import, and admin tooling each bind separately
  • Verify legitimate functionality still works (profile updates, settings changes)
  • Re-scan to confirm the issue is resolved

Common Vulnerable Patterns

The attribute name comes from the request, so the caller decides what gets written.

Dynamic Attribute Assignment from User Input (Pseudocode)

# Dangerous: sets attribute from user input
setattr(obj, user_input, value)

Why this is vulnerable: The caller chooses the attribute name, so every field on the object is writable - including is_admin, role and password_hash. Setting one of those turns an ordinary profile update into privilege escalation.

Secure Patterns

Allowlist-Based Attribute Assignment (Pseudocode)

# Safe: use allowlist for attributes
allowed_attrs = {'name', 'email', 'phone'}
if user_input in allowed_attrs:
    setattr(obj, user_input, value)
else:
    raise SecurityError('Attribute not allowed')

Why this works: the set of writable attributes is fixed in the code, so is_admin, role and password_hash stay out of reach whatever the caller sends.

Common Pitfalls

  • Allowlisting fields on only one endpoint that binds the model: restricting isAdmin/role on the create-user endpoint's binding, but leaving a separate bulk-update, PATCH, or admin-import endpoint that binds the same object without the same restriction - the allowlist protects one entry point while an attacker simply uses the other one to set the same field.
  • Looking for a model-level control the framework does not have, and stopping there when it is missing: the allowlist lives where each framework puts it, and for most of them that is the binding layer, not the model. Rails removed attr_accessible in 4.0 and Strong Parameters is per-action by design; ASP.NET Core and Spring bind to DTOs; DRF declares fields on the serializer. Laravel's $fillable is the exception rather than the rule. What matters is how many places bind the object and whether each of them has its own restriction. Count the non-HTTP ones too: a background job, a console command, a CSV importer, or an internal service call constructs the same object without passing through any controller's allowlist.
  • Blocking known-dangerous field names instead of allowlisting permitted ones: rejecting is_admin, role, and __proto__ by name (a denylist) rather than defining the small set of fields a caller may set - a denylist misses any security-critical field the author didn't think of, and new attributes added later are unprotected by default instead of protected by default.
  • Allowlisting the field name but not restricting who can set it: permitting role to be updated through a generic profile-update endpoint because some legitimate use case needs to change it, without a separate authorization check confirming the caller is allowed to change that attribute on that object - the attribute is on the allowlist, so any authenticated user can now set it on their own record.

Language-Specific Guidance

  • C# - ASP.NET Core model binding, ViewModels/DTOs, and why [Bind] is not a fix for a JSON API
  • Java - Spring MVC/Boot binding, DTOs, @InitBinder, Bean Validation
  • JavaScript/Node.js - filtering request bodies before passing to model constructors/updates
  • PHP - Laravel $fillable/$guarded, avoiding Model::create($request->all())
  • Python - Django/DRF serializer allowlists, avoiding fields = '__all__' for input
  • Ruby - Rails Strong Parameters, avoiding unfiltered update calls

Additional Resources