Vulnerability: SQL Injection in Moodle An SQL injection risk was identified in Badges code relating to configuring criteria. Access to the relevant capability was limited to teachers and managers by default.
Vulnerability ID: GHSA-h2fw-93qx-vrcq
CWE: CWE-89
Severity score: 8.8
Project: https://github.com/moodle/moodle
Published: 2022-03-26
File: badges/criteria/award_criteria_profile.php
Commit message: MDL-74074 badges: Ensure profile criteria exists before completion check
Commit author: Michael Hawkins

Vulnerable version (before fix):
 class award_criteria_profile extends award_criteria {     public $required_param = 'field';     public $optional_params = array();      /**      * Add appropriate new criteria options to the form      * public function get_options(&$mform) {         $none = true;         $existing = array();         $missing = array();          // Note: cannot use user_get_default_fields() here because it is not possible to decide which fields user can modify.         $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2',                          'department', 'institution', 'description', 'picture', 'city', 'country');          // Get custom fields.         $cfields = array_filter(profile_get_custom_fields(), function($field) { public function get_completed_criteria_sql() {                 $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";                 $params["fieldid{$idx}"] = $param['field'];                 $whereparts[] = "uid{$idx}.id IS NOT NULL";             } else {                 // This is a field from {user} table.                 if ($param['field'] == 'picture') {                     // The picture field is numeric and requires special handling.                     $whereparts[] = "u.{$param['field']} != 0";

Fixed version (after fix):
 class award_criteria_profile extends award_criteria {     public $required_param = 'field';     public $optional_params = array();      /* @var array The default profile fields allowed to be used as award criteria.      *      * Note: This is used instead of user_get_default_fields(), because it is not possible to      * determine which fields the user can modify.      */     protected $allowed_default_fields = [         'firstname',         'lastname',         'email',         'address',         'phone1',         'phone2',         'department',         'institution',         'description',         'picture',         'city',         'country',     ];      /**      * Add appropriate new criteria options to the form      * public function get_options(&$mform) {         $none = true;         $existing = array();         $missing = array();         $dfields = $this->allowed_default_fields;          // Get custom fields.         $cfields = array_filter(profile_get_custom_fields(), function($field) { public function get_completed_criteria_sql() {                 $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";                 $params["fieldid{$idx}"] = $param['field'];                 $whereparts[] = "uid{$idx}.id IS NOT NULL";             } else if (in_array($param['field'], $this->allowed_default_fields)) {                 // This is a valid field from {user} table.                 if ($param['field'] == 'picture') {                     // The picture field is numeric and requires special handling.                     $whereparts[] = "u.{$param['field']} != 0";

Patch:
patch: @@ -39,6 +39,26 @@ class award_criteria_profile extends award_criteria {
     public $required_param = 'field';
     public $optional_params = array();
 
+    /* @var array The default profile fields allowed to be used as award criteria.
+     *
+     * Note: This is used instead of user_get_default_fields(), because it is not possible to
+     * determine which fields the user can modify.
+     */
+    protected $allowed_default_fields = [
+        'firstname',
+        'lastname',
+        'email',
+        'address',
+        'phone1',
+        'phone2',
+        'department',
+        'institution',
+        'description',
+        'picture',
+        'city',
+        'country',
+    ];
+
     /**
      * Add appropriate new criteria options to the form
      *
@@ -50,10 +70,7 @@ public function get_options(&$mform) {
         $none = true;
         $existing = array();
         $missing = array();
-
-        // Note: cannot use user_get_default_fields() here because it is not possible to decide which fields user can modify.
-        $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2',
-                         'department', 'institution', 'description', 'picture', 'city', 'country');
+        $dfields = $this->allowed_default_fields;
 
         // Get custom fields.
         $cfields = array_filter(profile_get_custom_fields(), function($field) {
@@ -230,8 +247,8 @@ public function get_completed_criteria_sql() {
                 $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";
                 $params["fieldid{$idx}"] = $param['field'];
                 $whereparts[] = "uid{$idx}.id IS NOT NULL";
-            } else {
-                // This is a field from {user} table.
+            } else if (in_array($param['field'], $this->allowed_default_fields)) {
+                // This is a valid field from {user} table.
                 if ($param['field'] == 'picture') {
                     // The picture field is numeric and requires special handling.
                     $whereparts[] = "u.{$param['field']} != 0";
path: badges/criteria/award_criteria_profile.php