User Controlled Ownership Field
ID |
php.user_controlled_ownership_field |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Access Control |
Language |
Php |
Tags |
ASVS50:v8.1.1, CWE:639, CWE:915, NIST.SP.800-53, OWASP:2025:A01, PCI-DSS:6.5.8 |
Description
Data that came from the request reaches the property of a persisted model that says whose record it is — the customer id, the owner id, the tenant id, whatever this application calls it. The caller therefore decides which account a record belongs to: they can create a record under somebody else’s name, or move one of somebody else’s to themselves.
The detector reports three ways the value gets there:
-
the direct property write,
$order→customer_id = $id; -
Eloquent’s mass assignment —
Order::create($data),$order→fill($data),$order→update($data)— where the property is never named at all, because the call writes every attribute the request carried; -
the setter,
$order→setCustomerId($id), in either the PSR or the snake-case spelling.
Rationale
This is CWE-915 (mass assignment) read as CWE-639 (authorization bypass through a user-controlled key): the property being assigned is not just any attribute, it is the one the authorization decision is later made on. Every subsequent "is this record yours?" check answers yes, because the request already said so.
Mined from the code |
That the value is request-derived, and that it reaches the write: both are decided by the tainting
analysis against the shipped library metadata. No profile is involved. The model a write lands on is
resolved through the file’s |
Needs a profile fact |
Which property denotes the model’s owner. Nothing in a rule catalogue can express it, because the
next application calls the property something else; the application’s own convention profile names
it ( |
Without that fact this rule has no sink and reports nothing, and says so in the scan log rather
than reporting a clean result it could not establish. Every finding carries the fact it used and the
file:line where that fact can be checked.
The boundary with the API Security mass-assignment detector is the provenance of the property name:
that detector keeps the generic vocabulary (admin, role, scopes, balance) decided from the
endpoint surface, and this one keeps the property the application’s profile named, with a flow
proving the value reaches the model.
What the mass assignment can be shown to write
A mass assignment is reported only where the owner property can reach it. Two shapes say their keys out loud and are read:
-
a keyed array literal —
$order→fill(['total' ⇒ $t, 'customer_id' ⇒ $c])— where every key is written there, so a literal that does not name the owner property cannot write it; -
a narrowed request —
$request→only([…])and$request→except([…])— whose literal list of names is an allowlist and a denylist respectively.
Anything else, $request→all() or a variable, leaves the keys unknown and the write is reported: a
silence there would be a finding withdrawn on a guess.
$fillable and $guarded
A mass assignment writes only what the model admits, so a $fillable that leaves the owner property
out — or a $guarded that names it — withdraws the finding. That is read from the model’s own
declaration, and only when the model is declared in the file being analysed: an analysis of one file
cannot see a class declared in another. Where the declaration is out of reach the write is reported.
Remediation
Never let the request choose the owner. Set the property from the authenticated user on the server side, and keep it out of what is mass assigned:
public function store(Request $request)
{
$order = Order::create([
'total' => $request->input('total'),
'customer_id' => $request->user()->id, // server-derived, not from the request
]);
return $order;
}
Declare $fillable explicitly and leave the owner property out of it — never $guarded = [], which
admits everything. Validating the submitted value against the caller is a weaker fix than not
accepting it, but it is a fix — reject the request when the submitted owner is not the caller.