-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Major - P3
-
Affects Version/s: 2.0
-
Component/s: None
-
None
-
0.2
-
Dotnet Drivers
-
Not Needed
-
None
-
None
-
None
-
None
-
None
-
None
Summary
UpdateDefinitionBuilder selects a different wire encoding when a value list contains exactly one element. As a result PullAll, PushEach, and AddToSetEach violate their documented contracts at length 1: PullAll deletes array elements the caller never specified, and PushEach/AddToSetEach allow a document-shaped value to be interpreted as $push modifiers rather than stored as data.
Primary defect: unintended deletion via PullAll
PullAll is an exact-value removal API, but at length 1 it renders as $pull, whose document argument the server evaluates as a match condition using subset semantics.
Unable to find source-code formatter for language: csharp. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yaml// stored: items = [ {a:1,b:2}, {a:1} ] Builders<BsonDocument>.Update.PullAll("items", new[]{ new BsonDocument("a",1) }) // renders { $pull: { items: {a:1} } } -> items = [] both removed Builders<BsonDocument>.Update.PullAll("items", new[]{ new BsonDocument("a",1), new BsonDocument("a",99) }) // renders { $pullAll: { items: [{a:1}] } } -> items = [{a:1,b:2}] correct
This requires no attacker, no $-prefixed keys, and affects typed POCOs. Realistic triggers are [BsonIgnoreIfNull] / [BsonIgnoreIfDefault] on the item type, or schema drift where stored elements carry fields the current POCO no longer maps: $pullAll then silently removes nothing while $pull removes too much, and which behavior occurs depends on the caller's list length.
Secondary defect: data interpreted as $push modifiers
PushEach/AddToSetEach at length 1 serialize the lone value directly into modifier position, where $each, $slice, $position, and $sort inside the value are honored by the server.
items = [1,2,3,4]; PushEach("items", [ {"$each":[], "$slice":0} ]) renders { $push: { items: { $each: [], $slice: 0 } } } -> items = [] items = [1]; PushEach("items", [ {"$each":[7,8,9]} ]) renders { $push: { items: { $each: [7,8,9] } } } -> items = [1,7,8,9]
With two or more values the same payload is stored as inert data, so list length alone changes whether caller data is treated as data or as commands. Exploitation requires a loosely-typed item type, verbatim pass-through with no server-added fields (any extra field yields BadValue), and a payload of only modifier keys — narrow, which is why SECBUG-4301 was triaged Medium rather than High.
Root cause — four sites, all keyed on _values.Count == 1
| Location | Current behavior |
|---|---|
| UpdateDefinitionBuilder.cs:1586 | WriteName(_values.Count == 1 ? "$pull" : "$pullAll") |
| UpdateDefinitionBuilder.cs:1589 | single value serialized bare instead of into an array |
| UpdateDefinitionBuilder.cs:1374 | $addToSet single value serialized bare instead of {$each:[v]} |
| UpdateDefinitionBuilder.cs:1656 | $push single value serialized bare instead of {$each:[v]} |
Proposed fix
Render unconditionally. Placing caller data one level inside an array is a structural fix — array elements have no operator grammar, so no shape ambiguity remains.
Explicitly not a validation fix. Rejecting $-prefixed keys would be a blocklist needing maintenance as the server adds push modifiers, and would break legitimate dollar-prefixed data, permitted in stored documents since server 5.0.
Semver impact
This changes serialized BSON shape on unchanged public signatures, which AGENTS.md classifies as a breaking change. So not sure if we want to backport this or just target v4.0.
- The $each changes are behaviorally identical for legitimate data; the observable break is limited to tests asserting on rendered update documents.
- The PullAll change is a genuine semantic change. Applications currently relying — knowingly or not — on the one-element subset match will find those deletions stop occurring, with no exception raised. Release notes must call this out explicitly, since it produces neither a compile error nor a test failure in an affected application, only deletions that silently stop happening.
Test coverage
UpdateDefinitionBuilderTests.cs currently has no single-element case for PullAll, PushEach, or AddToSetEach. Add rendering tests for the one-element form of each, plus integration coverage asserting that a document-shaped value containing $each/$slice is stored as data rather than honored as modifiers, and that a one-element PullAll removes only exact matches.
Out of scope
Pull(field, value) maps to $pull by design, so its argument is a match condition at any count — unchanged by this fix and identical in Java. A doc-comment clarification is worth considering separately. Field-name injection is tracked separately in CSHARP-6154.