Query builder: force literal equality when 3-arg where uses '=' with an array value

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Critical - P2
    • 5.11.0
    • Affects Version/s: None
    • None
    • 5
    • 0.2
    • Needed
    • Hide

      1. What would you like to communicate to the user about this feature?

      where()` and MQL injection

      With 1 or 2 arguments, an array value is read as a MongoDB operator document.
      Never pass unvalidated input there. The 3-argument form with `'='` compares the
      value as a literal, and identifier columns (`_id`, `id`, `*._id`) reject operator
      arrays instead.

      2. Would you like the user to see examples of the syntax and/or executable code and its output?

       

      Post::where('status', ['$ne' => 'draft'])->get();             // operator document
      Post::where('status', $request->input('status'))->get();      // DON'T DO THIS
      Post::where('status', '=', $request->input('status'))->get(); // safe: literal match 

      3. Which versions of the driver/connector does this apply to?

      Breaking change in Laravel-MongoDB 5.11.0

      Show
      1. What would you like to communicate to the user about this feature? where()` and MQL injection With 1 or 2 arguments, an array value is read as a MongoDB operator document. Never pass unvalidated input there. The 3-argument form with `'='` compares the value as a literal, and identifier columns (`_id`, `id`, `*._id`) reject operator arrays instead. 2. Would you like the user to see examples of the syntax and/or executable code and its output?   Post::where( 'status' , [ '$ne' => 'draft' ])->get();             // operator document Post::where( 'status' , $request->input( 'status' ))->get();       // DON'T DO THIS Post::where( 'status' , '=' , $request->input( 'status' ))->get(); // safe: literal match 3. Which versions of the driver/connector does this apply to? Breaking change in Laravel-MongoDB 5.11.0
    • None
    • None
    • None
    • None
    • None
    • None

      Context

      Reported via SECBUG-4402 (Aegis-IH). See that ticket for the full analysis and reproduction script.

      Problem

      In the query builder, when where($column, '=', $value) is called with an array $value, the compiled MongoDB filter embeds the array as-is:

      where('token', '=', ['$ne' => null])
      // compiles to
      ['token' => ['$ne' => null]]
      

      The array is interpreted by MongoDB as an operator document, which turns an intended equality check into an arbitrary predicate. This also affects the internal call sites find($id) and delete($id), both of which route through where('_id', '=', $id).

      An application that forwards unvalidated request input to these entry points can be tricked into:

      • find(['$ne' => null]) returning the first document, bypassing an id lookup.
      • delete(['$ne' => null]) removing every document in the collection.
      • where('token', '=', $userInput) matching every row when the input is an operator document.

      Repro script attached to SECBUG-4402.

      Scope of the fix

      Only the 3-argument form with an explicit = or eq operator should be hardened. The developer explicitly expressed equality intent, so an array value must be compared as a literal document, not as an operator document.

      The 2-argument form where($column, $arrayValue) must keep its current behavior: it is a documented laravel-mongodb pattern for building operator documents (for example whereNot('title', ['$in' => ['admin']]), where('$text', ['$search' => 'x'])).

      Proposed change

      In the where() override in src/Query/Builder.php, after the existing operator normalization, when:

      • func_num_args() >= 3, and
      • $operator is '=' or 'eq', and
      • is_array($value)

      wrap the value in an explicit $eq:

      $value = ['$eq' => $value];
      

      Result:

      • where('token', '=', ['$ne' => null]) compiles to ['token' => ['$eq' => ['$ne' => null]]], which MongoDB treats as a literal document comparison and matches nothing unless the field actually stores that exact sub-document.
      • find(['$ne' => null]) and delete(['$ne' => null]) are neutralized through the same code path.
      • where('token', ['$ne' => null]) is unchanged (2-arg form).
      • where('foo', '$type', 2) is unchanged (operator not =).
      • where('$text', ['$search' => 'x']) is unchanged (2-arg form).
      • whereNot('title', ['$in' => ['admin']]) is unchanged (different where type).

      Tests

      Add cases in tests/Query/BuilderTest.php covering each vector above, plus the internal call sites find and delete.

      Docs / changelog

      Document the intentional divergence: in laravel-mongodb, where('x', $v) and where('x', '=', $v) are no longer equivalent when $v is an array. The 3-arg form enforces literal equality; the 2-arg form remains the operator-document shortcut. Note this in the upgrade guide and the query builder documentation.

              Assignee:
              Jérôme Tamarelle
              Reporter:
              Jérôme Tamarelle
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

                Created:
                Updated:
                Resolved: