ExportXMLWordPrintableJSON

    • 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.

            Assignee:
            Adelin Mbida Owona
            Reporter:
            Adelin Mbida Owona
            None
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: