-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Critical - P2
-
Affects Version/s: None
-
None
-
5
-
0.4
-
Not Needed
-
None
-
None
-
None
-
None
-
None
-
None
Context
Reported via SECBUG-4403 and SECBUG-4406 (Aegis-IH). See those tickets for the full analysis and reproduction scripts. This is the umbrella fix for the class of "a MongoDB operator document is smuggled into a position where only a scalar id or relation key is valid".
Related but independent: PHPLARA-260 hardens the general 3-argument where($column, '=', $array) equality by wrapping the value in $eq. That change happens to neutralize the single-equality relation paths, but it does not cover the embedded pull() path or whereIn eager loading, which this ticket addresses with an explicit id guard.
Problem
convertKey() in src/Query/Builder.php is the shared normalizer for document ids. It returns any value that is not a 24-hex or 16-byte string unchanged, arrays included. Wherever the library uses a value that can only legitimately be a scalar id or relation key, an attacker-supplied array such as {{
{"$ne": null}}} survives verbatim into the compiled MongoDB filter and becomes a live operator document.
Confirmed attack surfaces:
- Polymorphic relation lookup (MorphTo): a stored operator array on the morph foreign key resolves to {_id: {$ne: null}} and returns an arbitrary document (SECBUG-4403).
- Embedded document keys (EmbedsMany performDelete via pull(), performUpdate): an operator array as the embedded key deletes or overwrites every embedded document (SECBUG-4406).
Root cause
- convertKey() (src/Query/Builder.php) does no shape check on the id value.
- EmbedsOneOrMany::getForeignKeyValue() only calls convertKey, so the embedded key is never validated.
- Relation constraints (BelongsTo, MorphTo, BelongsToMany, MorphToMany) build the key comparison from a raw model attribute; when the key is a custom (non _id) column, the value never reaches the _id conversion path at all.
- DocumentModel::setAttribute() only converts 24-char string _id/id values, so an operator array assigned to the primary key is stored verbatim (plant variant).
Proposed fix
Reject any value that is a MongoDB operator document (an array containing one or more $-prefixed keys, checked recursively) in every position where only a scalar id or relation key is valid. Plain arrays without operator keys remain allowed, preserving support for composite _id documents.
Central guard plus the paths that bypass it:
- Add the operator-document check in convertKey() (src/Query/Builder.php). This covers find(), delete($id), where('_id', ...), whereKey, update, whereIn('_id', ...) per element, the default _id relation lookups, and EmbedsOneOrMany::getForeignKeyValue().
- Validate the key value in the relation constraints that can use a custom (non _id) key: BelongsTo::addConstraints, MorphTo::addConstraints, BelongsToMany::setWhere, and the whereIn eager-loading and pivot paths in MorphToMany.
- Reject an operator array assigned to _id/id in DocumentModel::setAttribute() (write-time planting).
Mirror the existing style of the column guard in where() (src/Query/Builder.php), which throws InvalidArgumentException with get_debug_type().
Tests
Add cases covering: find, delete, where('_id', ...), morphTo resolution, embedded delete and update, belongsTo and belongsToMany with a custom key, whereIn('_id', ...), and setAttribute('_id', ...). Assert that an operator array throws, that a scalar id still works, and that a plain composite array id is still accepted. Use the repro scripts attached to SECBUG-4403 and SECBUG-4406 as the basis.
Docs / changelog
Document that passing a MongoDB operator document where an id or relation key is expected now throws.