NoSQL injection via unquoted constant GroupBy keys in LINQ pipeline translation

XMLWordPrintableJSON

    • None
    • 0.2
    • Fully Compatible
    • Dotnet Drivers
    • Not Needed
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      User data captured into a LINQ GroupBy constant reaches the database unescaped, letting dollar-prefixed strings read hidden fields or invoke server-side operators.

      Impact

      The MongoDB server evaluates the attacker's content as aggregation expressions inside $group, disclosing arbitrary fields of every grouped document through the app's result path, silently corrupting grouping results, and — with key control on scripting-enabled servers — executing attacker JavaScript via $function.
      Severity: high (upper bound critical)
      Exploitability: likely_exploitable — Exploitation is realistic wherever an app embeds attacker strings inside a document/array-shaped GroupBy key or element constant — a common pattern (grouping by a user-selected composite key); the attacker fully controls the injected field path or operator.
      Customer data: customer_content, pii, credentials (api_response)

      Location

      • src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/GroupByMethodToPipelineTranslator.cs:55 in Translate
      • src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstConstantExpression.cs:45 in Render
      • src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstGroupStage.cs:51 in Render
      • src/MongoDB.Driver/Linq/Linq3Implementation/GroupingWithOutputExpressionStageDefinitions.cs:201 in RenderGroupStage

      Reproduction / trigger path

      Analyzed trigger path from static analysis — not an executed PoC. Confirm with a concrete repro before handing off.

      Attacker controls: Attacker controls string values nested in the constant (field-path injection: '$anyField', '$$variable') and, when the app builds the constant from a dictionary with attacker-supplied keys, the document keys themselves (operator injection: $cond, $function, etc.).

      1. (source) src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LinqExpressionPreprocessor.cs:30 in Preprocess — PartialEvaluator folds the parameter-independent GroupBy selector body — carrying attacker-supplied strings captured in a closure object, dictionary, or array — into a ConstantExpression.
      2. (hop) src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/GroupByMethodToPipelineTranslator.cs:47 in Translate — TranslateLambdaBody translates the key selector (and element selector at line 81), dispatching the ConstantExpression to the constant translator.
      3. (hop) src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ConstantExpressionToAggregationExpressionTranslator.cs:35 in Translate — Serializes the constant to an arbitrary BsonValue shape (document/array with attacker strings and keys) and wraps it in AstConstantExpression.
      4. (hop) src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/GroupByMethodToPipelineTranslator.cs:55 in Translate — Passes the constant AST directly as the $group _id (line 55) and $push accumulator argument (line 56) with no quoting step.
      5. (hop) src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstConstantExpression.cs:45 in Render — Quoting gap: only a top-level $-prefixed BsonString is $literal-wrapped (lines 40-43); constant documents and arrays with nested $-prefixed content are returned raw.
      6. (sink) src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstGroupStage.cs:51 in Render — The raw constant is embedded into the $group command document sent to the MongoDB server, whose aggregation evaluator interprets $-prefixed strings as field paths and $-prefixed keys as operators.

      Root cause

      • PartialEvaluator folds any parameter-independent GroupBy key/element selector (closure-captured objects, dictionaries, arrays holding user data) into a ConstantExpression (src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LinqExpressionPreprocessor.cs:30).
      • ConstantExpressionToAggregationExpressionTranslator serializes the constant to an arbitrary BsonValue and wraps it in AstConstantExpression (ConstantExpressionToAggregationExpressionTranslator.cs:35-36); GroupByMethodToPipelineTranslator passes that AST directly as the $group _id and $push argument (GroupByMethodToPipelineTranslator.cs:55-56).
      • AstConstantExpression.Render() only $literal-wraps a top-level $-prefixed BsonString; constant documents and arrays — including nested $-prefixed values and keys — are returned raw (AstConstantExpression.cs:40-45), and AstGroupStage.Render() embeds them verbatim into the $group command document (AstGroupStage.cs:51).
      • The existing safeguard, ProjectionHelper.QuoteIfNecessary (ProjectionHelper.cs:140-165), $literal-quotes document/boolean/numeric constants but is only invoked on the $project paths — never on the $group path; AstSimplifier and AstGroupingPipelineOptimizer add no quoting either.

      Ownership

      Assigned teams: @mongodb/dbx-csharp-dotnet


      Filed from Aegis finding 60f097f03309 (scan scan-320b0a32a42c) · primitive: Nested dollar-prefixed strings and keys in constant GroupBy selectors render unquoted into the group stage, becoming live server-side aggregation expressions. · categories: NoSQL Injection, PII Exposure, Data Corruption

      https://docs.google.com/document/d/1osPjSiI-pvMXtrI49QVqwrIK21_nU38fsp7tJlQpDlY/edit?tab=t.0#heading=h.biijq6ke6z0p

            Assignee:
            Damien Guard
            Reporter:
            Boris Dogadov
            None
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: