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.prototypeand 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__,constructorandprototypeas 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,priceorbalance?
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
rolethat 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
@propertyaccessors -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, andobj[key] = valueover a user-supplied key - Use the framework's own binding controls: ORM allowlists such as Django
fieldsor Railspermit, 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,permissionsandprice - 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
isAdminwhether 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)
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/roleon 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_accessiblein 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$fillableis 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
roleto 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, avoidingModel::create($request->all()) - Python - Django/DRF serializer allowlists, avoiding
fields = '__all__'for input - Ruby - Rails Strong Parameters, avoiding unfiltered
updatecalls