CVE-2020-35847: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value: Agentejo Cockpit before 0.11.2 allows NoSQL injection via the Controller/Auth.php resetpassword function.
CWE description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
CVSS v3 base score: 9.8
Published: 2020-12-30T01:15Z
Repository: https://github.com/agentejo/cockpit
Language: PHP
Files changed: lib/MongoLite/Database.php
Commit date: 2020-09-25 23:18:28 +0200
Commit message: remove $fn, $func, $f as field filter ... add $where as an alternative (MongoLite)

Diff: diff --git a/lib/MongoLite/Database.php b/lib/MongoLite/Database.php
index 08141f40..968b4484 100644
--- a/lib/MongoLite/Database.php
+++ b/lib/MongoLite/Database.php
@@ -233,10 +233,16 @@ class Database {
 }
 
 
 class UtilArrayQuery {
 
+    protected static $closures = [];
+
+    public static function closureCall($uid, $doc) {
+        return call_user_func_array(self::$closures[$uid], [$doc]);
+    }
+
     public static function buildCondition($criteria, $concat = ' && ') {
 
         $fn = [];
 
         foreach ($criteria as $key => $value) {
@@ -266,15 +272,20 @@ class UtilArrayQuery {
 
                     break;
 
                 case '$where':
 
-                    if (\is_callable($value)) {
-
-                        // need implementation
+                    if (\is_string($value) || !\is_callable($value)) {
+                        throw new \InvalidArgumentException($key.' Function should be callable');
                     }
 
+                    $uid = \uniqid('mongoliteCallable').bin2hex(random_bytes(5));
+
+                    self::$closures[$uid] = $value;
+
+                    $fn[] = '\\MongoLite\\UtilArrayQuery::closureCall("'.$uid.'", $document)';
+
                     break;
 
                 default:
 
                     $d = '$document';
@@ -424,18 +435,10 @@ class UtilArrayQuery {
                 if (! \is_array($b))
                     throw new \InvalidArgumentException('Invalid argument for $mod option must be array');
                 $r = $a % $b[0] == $b[1] ?? 0;
                 break;
 
-            case '$func' :
-            case '$fn' :
-            case '$f' :
-                if (\is_string($b) || !\is_callable($b))
-                    throw new \InvalidArgumentException('Function should be callable');
-                $r = $b($a);
-                break;
-
             case '$exists':
                 $r = $b ? !\is_null($a) : \is_null($a);
                 break;
 
             case '$fuzzy':

Vulnerable code:                     if (\is_callable($value)) {

                        // need implementation
            case '$func' :
            case '$fn' :
            case '$f' :
                if (\is_string($b) || !\is_callable($b))
                    throw new \InvalidArgumentException('Function should be callable');
                $r = $b($a);
                break;

Fixed code:     protected static $closures = [];

    public static function closureCall($uid, $doc) {
        return call_user_func_array(self::$closures[$uid], [$doc]);
    }

                    if (\is_string($value) || !\is_callable($value)) {
                        throw new \InvalidArgumentException($key.' Function should be callable');
                    $uid = \uniqid('mongoliteCallable').bin2hex(random_bytes(5));

                    self::$closures[$uid] = $value;

                    $fn[] = '\\MongoLite\\UtilArrayQuery::closureCall("'.$uid.'", $document)';