VYPR
Moderate severityNVD Advisory· Published Apr 29, 2010· Updated Apr 29, 2026

CVE-2010-1616

CVE-2010-1616

Description

In Moodle 1.8.x and 1.9.x before 1.9.8, a teacher can create new user accounts during course restore even without the required moodle/user:create capability.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In Moodle 1.8.x and 1.9.x before 1.9.8, a teacher can create new user accounts during course restore even without the required moodle/user:create capability.

Vulnerability

Moodle versions 1.8.x and 1.9.x before 1.9.8 contain a privilege escalation vulnerability in the course restore functionality. When restoring a course, the system can create new roles and user accounts without properly checking the restoring user's capabilities [1][4]. Specifically, the restore_create_users() function does not enforce the moodle/user:create capability check during the restore process [3]. This affects all installations using the affected versions [4].

Exploitation

An authenticated teacher (with the moodle/restore:createfrombackup capability) can exploit this by initiating a course restore from a backup file that contains user accounts. The restore process will create those users even if the teacher lacks the moodle/user:create capability [1][4]. The patch introduces a new restore:createuser capability check that was missing in earlier versions [3]. No special network position or additional authentication is required beyond being an authorized teacher.

Impact

A teacher can create new user accounts without authorization, potentially leading to unauthorized access to the Moodle site. The attacker gains the ability to add arbitrary users (including students or other roles) to courses, bypassing the intended access control [2][4]. This could be used to grant course access to unauthorized individuals or to create privileged accounts.

Mitigation

The vulnerability is fixed in Moodle version 1.9.8 and later [4]. The fix adds a new restore:createuser capability check in the restore process, ensuring that only users with that capability can create accounts during restore [3]. Administrators should upgrade to Moodle 1.9.8 or apply the patches referenced in commits [1][2][3]. There are no known workarounds for unsupported versions. This CVE is not listed in CISA's Known Exploited Vulnerabilities (KEV) catalog.

AI Insight generated on May 23, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
moodle/moodlePackagist
>= 1.8.0, < 1.8.121.8.12
moodle/moodlePackagist
>= 1.9.0, < 1.9.81.9.8

Affected products

19
  • Moodle/Moodle18 versions
    cpe:2.3:a:moodle:moodle:1.8.1:*:*:*:*:*:*:*+ 17 more
    • cpe:2.3:a:moodle:moodle:1.8.1:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.10:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.11:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.2:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.3:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.4:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.5:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.6:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.7:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.8:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.8.9:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.1:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.2:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.3:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.4:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.5:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.6:*:*:*:*:*:*:*
    • cpe:2.3:a:moodle:moodle:1.9.7:*:*:*:*:*:*:*
  • ghsa-coords
    Range: >= 1.8.0, < 1.8.12

Patches

5
b0ccfc5ce87f

config settings: MDL-16658 small rewording

https://github.com/moodle/moodleHelen FosterJan 5, 2010via ghsa
1 file changed · +11 10
  • config-dist.php+11 10 modified
    @@ -180,19 +180,20 @@
     // shared out from your site/institution!
     //      $CFG->includeuserpasswordsinbackup = true;
     //
    -// Completely disable user creation on restore, bypassing any right granted
    -// via roles/capabilities. By enabling this setting any restore process
    -// requiring users to be created will be stopped before any further action.
    +// Completely disable user creation when restoring a course, bypassing any
    +// permissions granted via roles and capabilities. Enabling this setting
    +// results in the restore process stopping when a user attempts to restore a
    +// course requiring users to be created.
     //     $CFG->disableusercreationonrestore = true;
     //
     // Modify the restore process in order to force the "user checks" to assume
    -// that the backup was originated in a different site so detection of matching
    -// users is performed with some different (more "relaxed") rules. *Only* useful
    -// if the backup file has been generated with Moodle < 1.9.4 and the site has
    -// been rebuilt from scratch using backup files (not the best way, btw). If you
    -// are getting user conflicts on restore, try to restore the backup to a different
    -// site, backup it again and then restore on target server instead of enabling this
    -// setting permanently!
    +// that the backup originated from a different site, so detection of matching
    +// users is performed with different (more "relaxed") rules. Note that this is
    +// only useful if the backup file has been created using Moodle < 1.9.4 and the
    +// site has been rebuilt from scratch using backup files (not the best way btw).
    +// If you obtain user conflicts on restore, rather than enabling this setting
    +// permanently, try restoring the backup on a different site, back it up again
    +// and then restore on the target server.
     //    $CFG->forcedifferentsitecheckingusersonrestore = true;
     //
     // Prevent stats processing and hide the GUI
    
d8ada21339ec

moodle lang string: MDL-16658 small rewording

https://github.com/moodle/moodleHelen FosterJan 5, 2010via ghsa
1 file changed · +5 5
  • lang/en_utf8/moodle.php+5 5 modified
    @@ -1368,19 +1368,19 @@
     $string['resource'] = 'Resource';
     $string['resources'] = 'Resources';
     $string['restore'] = 'Restore';
    -$string['restorecancelled'] = 'Restore canceled';
    -$string['restorecannotcreateuser'] = 'restore needs to create user \'$a\' from backup file and you are not allowed to do that';
    +$string['restorecancelled'] = 'Restore cancelled';
    +$string['restorecannotcreateuser'] = 'Restore needs to create user \'$a\' from backup file and you do not have permission to do so';
     $string['restorecoursenow'] = 'Restore this course now!';
     $string['restoredaccount'] = 'Restored account';
     $string['restoredaccountinfo'] = 'This account was imported from another server and the password has been lost. To set a new password by email, please click \"Continue\"';
     $string['restorefinished'] = 'Restore completed successfully';
     $string['restoreto'] = 'Restore to';
     $string['restoretositeadding'] = 'Warning: You are about to restore to the site front page, adding data to it!';
     $string['restoretositedeleting'] = 'Warning: You are about to restore to the site front page, deleting data from it first!';
    -$string['restoreuserconflict'] = 'trying to restore user \'$a\' from backup file will cause conflict';
    +$string['restoreuserconflict'] = 'Trying to restore user \'$a\' from backup file will cause conflict';
     $string['restoreuserinfofailed'] = 'The restore process has stopped because you don\'t have permission to restore user data.';
    -$string['restoreusersprecheck'] = 'Checking user information';
    -$string['restoreusersprecheckerror'] = 'Some problems were detected checking user information';
    +$string['restoreusersprecheck'] = 'Checking user data';
    +$string['restoreusersprecheckerror'] = 'Some problems were detected when checking user data';
     $string['restricted'] = 'Restricted';
     $string['restrictmodules'] = 'Restrict activity modules?';
     $string['returntooriginaluser'] = 'Return to $a';
    
5e934890c9fb

MDL-16658 restore - new restore:createuser cap + pre-check users before restoring; merged from 19_STABLE

https://github.com/moodle/moodleEloy LafuenteDec 30, 2009via ghsa
6 files changed · +66 61
  • backup/restorelib.php+34 60 modified
    @@ -742,7 +742,7 @@ function restore_check_user($restore, $user) {
             }
     
             // Handle checks from same site backups
    -        if (backup_is_same_site($restore)) {
    +        if (backup_is_same_site($restore) && empty($CFG->forcedifferentsitecheckingusersonrestore)) {
     
                 // 1A - If match by id and username and mnethost => ok, return target user
                 if ($rec = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
    @@ -950,7 +950,7 @@ function restore_precheck_users($xml_file, $restore, &$problems) {
                 $user = $rec->info;
     
                 // Find the correct mnethostid for user before performing any further check
    -            if (empty($user->mnethosturl) || $user->mnethosturl===$CFG->wwwroot) {
    +            if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
                     $user->mnethostid = $CFG->mnet_localhost_id;
                 } else {
                     // fast url-to-id lookups
    @@ -965,13 +965,18 @@ function restore_precheck_users($xml_file, $restore, &$problems) {
                 $usercheck = restore_check_user($restore, $user);
     
                 if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to
    +                // Annotate it, for later process by restore_create_users(). Set new_id to mapping user->id
    +                backup_putid($restore->backup_unique_code, 'user', $userid, $usercheck->id, $user);
     
                 } else if ($usercheck === false) { // Found conflict, report it as problem
                     $problems[] = get_string('restoreuserconflict', '', $user->username);
                     $status = false;
     
                 } else if ($usercheck === true) { // User needs to be created, check if we are able
    -                if (!$cancreateuser) { // Cannot create, report as problem
    +                if ($cancreateuser) { // Can create user, annotate it, for later process by restore_create_users(). Set new_id to 0
    +                    backup_putid($restore->backup_unique_code, 'user', $userid, 0, $user);
    +
    +                } else { // Cannot create user, report it as problem
     
                         $problems[] = get_string('restorecannotcreateuser', '', $user->username);
                         $status = false;
    @@ -2830,25 +2835,17 @@ function restore_create_users($restore,$xml_file) {
             $authcache = array(); // Cache to get some bits from authentication plugins
     
             $status = true;
    -        //Check it exists
    -        if (!file_exists($xml_file)) {
    -            $status = false;
    -        }
    -        //Get info from xml
    -        if ($status) {
    -            //info will contain the old_id of every user
    -            //in backup_ids->info will be the real info (serialized)
    -            $info = restore_read_xml_users($restore,$xml_file);
    -        }
     
    -        //Now, get evey user_id from $info and user data from $backup_ids
    -        //and create the necessary db structures
    +        // Users have already been checked by restore_precheck_users() so they are loaded
    +        // in backup_ids table. They don't need to be loaded (parsed) from XML again. Also, note
    +        // the same function has performed the needed modifications in the $user->mnethostid field
    +        // so we don't need to do it again here at all. Just some checks.
     
    -        if (!empty($info->users)) {
    +        // Get users ids from backup_ids table
    +        $userids = $DB->get_fieldset_select('backup_ids', 'old_id', 'backup_code = ? AND table_name = ?', array($restore->backup_unique_code, 'user'));
     
    -        /// Grab mnethosts keyed by wwwroot, to map to id
    -            $mnethosts = $DB->get_records('mnet_host', null,
    -                                     'wwwroot', 'wwwroot, id');
    +        // Have users to process, proceed with them
    +        if (!empty($userids)) {
     
             /// Get languages for quick search later
                 $languages = get_list_of_languages();
    @@ -2858,9 +2855,22 @@ function restore_create_users($restore,$xml_file) {
     
             /// Init trailing messages
                 $messages = array();
    -            foreach ($info->users as $userid) {
    -                $rec = backup_getid($restore->backup_unique_code,"user",$userid);
    -                $user = $rec->info;
    +            foreach ($userids as $userid) {
    +                // Defaults
    +                $user_exists = false; // By default user does not exist
    +                $newid = null;        // By default, there is not newid
    +
    +                // Get record from backup_ids
    +                $useridsdbrec = backup_getid($restore->backup_unique_code, 'user', $userid);
    +
    +                // Based in restore_precheck_users() calculations, if the user exists
    +                // new_id must contain the id of the matching user
    +                if (!empty($useridsdbrec->new_id)) {
    +                    $user_exists = true;
    +                    $newid = $useridsdbrec->new_id;
    +                }
    +
    +                $user = $useridsdbrec->info;
                     foreach (array_keys(get_object_vars($user)) as $field) {
                         if (!is_array($user->$field)) {
                             $user->$field = backup_todb($user->$field);
    @@ -2908,31 +2918,12 @@ function restore_create_users($restore,$xml_file) {
                     //Has role teacher or student or needed
                     $is_course_user = ($is_teacher or $is_student or $is_needed);
     
    -                // in case we are restoring to same server, look for user by id and username
    -                // it should return record always, but in sites rebuilt from scratch
    -                // and being reconstructed using course backups
    -                $user_data = false;
    -                if (backup_is_same_site($restore)) {
    -                    $user_data = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username));
    -                }
    -
                     // Only try to perform mnethost/auth modifications if restoring to another server
                     // or if, while restoring to same server, the user doesn't exists yet (rebuilt site)
                     //
                     // So existing user data in same server *won't be modified by restore anymore*,
                     // under any circumpstance. If somehting is wrong with existing data, it's server fault.
    -                if (!backup_is_same_site($restore) || (backup_is_same_site($restore) && !$user_data)) {
    -                    //Calculate mnethostid
    -                    if (empty($user->mnethosturl) || $user->mnethosturl===$CFG->wwwroot) {
    -                        $user->mnethostid = $CFG->mnet_localhost_id;
    -                    } else {
    -                        // fast url-to-id lookups
    -                        if (isset($mnethosts[$user->mnethosturl])) {
    -                            $user->mnethostid = $mnethosts[$user->mnethosturl]->id;
    -                        } else {
    -                            $user->mnethostid = $CFG->mnet_localhost_id;
    -                        }
    -                    }
    +                if (!backup_is_same_site($restore) || (backup_is_same_site($restore) && !$user_exists)) {
                         //Arriving here, any user with mnet auth and using $CFG->mnet_localhost_id is wrong
                         //as own server cannot be accesed over mnet. Change auth to manual and inform about the switch
                         if ($user->auth == 'mnet' && $user->mnethostid == $CFG->mnet_localhost_id) {
    @@ -2951,21 +2942,6 @@ function restore_create_users($restore,$xml_file) {
                     }
                     unset($user->mnethosturl);
     
    -                //To store user->id along all the iteration
    -                $newid=null;
    -                //check if it exists (by username) and get its id
    -                $user_exists = true;
    -                if (!backup_is_same_site($restore) || !$user_data) { /// Restoring to another server, or rebuilding site (failed id&
    -                                                                     /// login search above), look for existing user based on fields
    -                    $user_data = $DB->get_record('user', array('username'=>$user->username, 'mnethostid'=>$user->mnethostid));
    -                }
    -
    -                if (!$user_data) {
    -                    $user_exists = false;
    -                } else {
    -                    $newid = $user_data->id;
    -                }
    -
                     //Flags to see what parts are we going to restore
                     $create_user = true;
                     $create_roles = true;
    @@ -3245,7 +3221,7 @@ function restore_create_users($restore,$xml_file) {
                         }
                         backup_flush(300);
                     }
    -            } /// End of loop over all the users loaded from xml
    +            } /// End of loop over all the users loaded from backup_ids table
     
             /// Inform about all the messages geerated while restoring users
                 if (!defined('RESTORE_SILENTLY')) {
    @@ -8487,7 +8463,6 @@ function restore_execute(&$restore,$info,$course_header,&$errorstr) {
     
             // Precheck the users section, detecting various situations that can lead to problems, so
             // we stop restore before performing any further action
    -        /*
             if (!defined('RESTORE_SILENTLY')) {
                 echo '<li>'.get_string('restoreusersprecheck').'</li>';
             }
    @@ -8501,7 +8476,6 @@ function restore_execute(&$restore,$info,$course_header,&$errorstr) {
                 }
                 return false;
             }
    -        */
     
             //If we've selected to restore into new course
             //create it (course)
    
  • config-dist.php+15 0 modified
    @@ -180,6 +180,21 @@
     // shared out from your site/institution!
     //      $CFG->includeuserpasswordsinbackup = true;
     //
    +// Completely disable user creation on restore, bypassing any right granted
    +// via roles/capabilities. By enabling this setting any restore process
    +// requiring users to be created will be stopped before any further action.
    +//     $CFG->disableusercreationonrestore = true;
    +//
    +// Modify the restore process in order to force the "user checks" to assume
    +// that the backup was originated in a different site so detection of matching
    +// users is performed with some different (more "relaxed") rules. *Only* useful
    +// if the backup file has been generated with Moodle < 1.9.4 and the site has
    +// been rebuilt from scratch using backup files (not the best way, btw). If you
    +// are getting user conflicts on restore, try to restore the backup to a different
    +// site, backup it again and then restore on target server instead of enabling this
    +// setting permanently!
    +//    $CFG->forcedifferentsitecheckingusersonrestore = true;
    +//
     // Prevent stats processing and hide the GUI
     //      $CFG->disablestatsprocessing = true;
     //
    
  • lang/en_utf8/moodle.php+4 0 modified
    @@ -1369,14 +1369,18 @@
     $string['resources'] = 'Resources';
     $string['restore'] = 'Restore';
     $string['restorecancelled'] = 'Restore canceled';
    +$string['restorecannotcreateuser'] = 'restore needs to create user \'$a\' from backup file and you are not allowed to do that';
     $string['restorecoursenow'] = 'Restore this course now!';
     $string['restoredaccount'] = 'Restored account';
     $string['restoredaccountinfo'] = 'This account was imported from another server and the password has been lost. To set a new password by email, please click \"Continue\"';
     $string['restorefinished'] = 'Restore completed successfully';
     $string['restoreto'] = 'Restore to';
     $string['restoretositeadding'] = 'Warning: You are about to restore to the site front page, adding data to it!';
     $string['restoretositedeleting'] = 'Warning: You are about to restore to the site front page, deleting data from it first!';
    +$string['restoreuserconflict'] = 'trying to restore user \'$a\' from backup file will cause conflict';
     $string['restoreuserinfofailed'] = 'The restore process has stopped because you don\'t have permission to restore user data.';
    +$string['restoreusersprecheck'] = 'Checking user information';
    +$string['restoreusersprecheckerror'] = 'Some problems were detected checking user information';
     $string['restricted'] = 'Restricted';
     $string['restrictmodules'] = 'Restrict activity modules?';
     $string['returntooriginaluser'] = 'Return to $a';
    
  • lang/en_utf8/role.php+1 0 modified
    @@ -200,6 +200,7 @@
     $string['resetrolesure'] = 'Are you sure that you want to reset role \"$a->name ($a->shortname)\" to defaults?<p></p>The defaults are taken from the selected legacy capability ($a->legacytype).';
     $string['resetrolesurenolegacy'] = 'Are you sure that you want to clear all permissions defined in this role \"$a->name ($a->shortname)\"?';
     $string['restore:rolldates'] = 'Allowed to roll activity configuration dates on restore';
    +$string['restore:createuser'] = 'Create users on restore';
     $string['restore:userinfo'] = 'Restore user data';
     $string['risks'] = 'Risks';
     $string['role:assign'] = 'Assign roles to users';
    
  • lib/db/access.php+11 0 modified
    @@ -216,6 +216,17 @@
             )
         ),
     
    +    'moodle/restore:createuser' => array(
    +
    +        'riskbitmask' => RISK_SPAM | RISK_PERSONAL,
    +
    +        'captype' => 'write',
    +        'contextlevel' => CONTEXT_SYSTEM,
    +        'legacy' => array(
    +            'admin' => CAP_ALLOW
    +        )
    +    ),
    +
         'moodle/restore:userinfo' => array(
     
             'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_CONFIG,
    
  • version.php+1 1 modified
    @@ -6,7 +6,7 @@
     // This is compared against the values stored in the database to determine
     // whether upgrades should be performed (see lib/db/*.php)
     
    -    $version = 2009122500;  // YYYYMMDD   = date of the last version bump
    +    $version = 2009123000;  // YYYYMMDD   = date of the last version bump
                                 //         XX = daily increments
     
         $release = '2.0 dev (Build: 20091230)';  // Human-friendly version name
    
5d9ab024ac9c

MDL-16658 restore users pre-check. Reviewed logic with latest discussions. Continues disabled. Merged from 19_STABLE

https://github.com/moodle/moodleEloy LafuenteNov 29, 2009via ghsa
1 file changed · +101 36
  • backup/restorelib.php+101 36 modified
    @@ -701,22 +701,34 @@ function restore_print_course_header ($course_header) {
         * Here it's the logic applied, keep it updated:
         *
         *  If restoring users from same site backup:
    -    *      1A - If match by id and username and mnethost  => ok, return target user
    -    *      1B - If match by id and mnethost and user is deleted in DB and
    -    *           match by email LIKE 'backup_email%'  => ok, return target user
    -    *      1C - If match by id and mnethost and user is deleted in backup file
    +    *      1A - Normal check: If match by id and username and mnethost  => ok, return target user
    +    *      1B - Handle users deleted in DB and "alive" in backup file:
    +    *           If match by id and mnethost and user is deleted in DB and
    +    *           (match by username LIKE 'backup_email.%' or by non empty email = md5(username)) => ok, return target user
    +    *      1C - Handle users deleted in backup file and "alive" in DB:
    +    *           If match by id and mnethost and user is deleted in backup file
         *           and match by email = email_without_time(backup_email) => ok, return target user
    -    *      1D - If match by username and mnethost and doesn't match by id => conflict, return false
    -    *      1E - else => user needs to be created, return true
    +    *      1D - Conflict: If match by username and mnethost and doesn't match by id => conflict, return false
    +    *      1E - None of the above, return true => User needs to be created
         *
    -    *  if restoring from another site backup:
    -    *      2A - If match by username and mnethost and
    -    *           (email or non-zero firstaccess) => ok, return target user
    -    *      2B - Note: we cannot handle "deleted" situations here as far
    -    *           as username gets modified and id cannot be used here
    -    *      2C - If match by username and mnethost and not
    -    *           by (email or non-zero firstaccess) => conflict, return false
    -    *      2D - else => user needs to be created, return true
    +    *  if restoring from another site backup (cannot match by id here, replace it by email/firstaccess combination):
    +    *      2A - Normal check: If match by username and mnethost and (email or non-zero firstaccess) => ok, return target user
    +    *      2B - Handle users deleted in DB and "alive" in backup file:
    +    *           2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
    +    *                 (username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
    +    *           2B2 - If match by mnethost and user is deleted in DB and
    +    *                 username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
    +    *                 (to cover situations were md5(username) wasn't implemented on delete we requiere both)
    +    *      2C - Handle users deleted in backup file and "alive" in DB:
    +    *           If match mnethost and user is deleted in backup file
    +    *           and by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
    +    *      2D - Conflict: If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false
    +    *      1E - None of the above, return true => User needs to be created
    +    *
    +    * Note: for DB deleted users email is stored in username field, hence we
    +    *       are looking there for emails. See delete_user()
    +    * Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
    +    *       hence we are looking there for usernames if not empty. See delete_user()
         */
         function restore_check_user($restore, $user) {
             global $CFG, $DB;
    @@ -738,27 +750,35 @@ function restore_check_user($restore, $user) {
                 }
     
                 // 1B - Handle users deleted in DB and "alive" in backup file
    -            // 1B1- If match by id and mnethost and user is deleted in DB and
    -            //      match by email LIKE 'backup_email.%'  => ok, return target user
    -            // Note: for deleted users email is stored in username field, hence we
    -            //       are looking there for emails in the query below. See delete_user()
    +            // Note: for DB deleted users email is stored in username field, hence we
    +            //       are looking there for emails. See delete_user()
    +            // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
    +            //       hence we are looking there for usernames if not empty. See delete_user()
    +            // If match by id and mnethost and user is deleted in DB and
    +            // match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user
                 if ($rec = $DB->get_record_sql("SELECT *
                                                   FROM {user} u
                                                  WHERE id = ?
                                                    AND mnethostid = ?
                                                    AND deleted = 1
    -                                               AND username LIKE ?",
    -                                           array($user->id, $user->mnethostid, $user->email.'.%')) {
    +                                               AND (
    +                                                       username LIKE ?
    +                                                    OR (
    +                                                           ".$DB->sql_isnotempty('user', 'email', false, false)."
    +                                                       AND email = ?
    +                                                       )
    +                                                   )",
    +                                           array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) {
                     return $rec; // Matching user, deleted in DB found, return it
                 }
     
                 // 1C - Handle users deleted in backup file and "alive" in DB
    -            // 1C1- If match by id and mnethost and user is deleted in backup file
    -            //      and match by email = email_without_time(backup_email) => ok, return target user
    +            // If match by id and mnethost and user is deleted in backup file
    +            // and match by email = email_without_time(backup_email) => ok, return target user
                 if ($user->deleted) {
    +                // Note: for DB deleted users email is stored in username field, hence we
    +                //       are looking there for emails. See delete_user()
                     // Trim time() from email
    -                // Note: for deleted users email is stored in username field, hece
    -                //       we are trimming the username field to get the email. See delete_user()
                     $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
                     if ($rec = $DB->get_record_sql("SELECT *
                                                       FROM {user} u
    @@ -794,23 +814,68 @@ function restore_check_user($restore, $user) {
                                                            )
                                                        )",
                                                array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
    -                return $rec; // Matching user
    +                return $rec; // Matching user found, return it
                 }
     
                 // 2B - Handle users deleted in DB and "alive" in backup file
    -            // Note: for deleted users email is stored in username field, hence we
    -            //       are looking there for emails in the query below. See delete_user()
    -            // Note: for deleted users md5(username) is stored *sometimes* in the
    -            // email field, hence we are looking there for usernames in the query below
    -            // 2B - Note: we cannot handle "deleted" situations here as far
    -            //     as username gets modified and id cannot be used either
    -            // 2B1-deleted = 1 AND email = md5(username) AND mnsethostid AND (username like $user->email.% OR firstaccess)
    -            // 2B2 deleted and mnsethostid AND username like $user->email.% AND firstaccess
    +            // Note: for DB deleted users email is stored in username field, hence we
    +            //       are looking there for emails. See delete_user()
    +            // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
    +            //       hence we are looking there for usernames if not empty. See delete_user()
    +            // 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
    +            //       (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
    +            if ($rec = $DB->get_record_sql("SELECT *
    +                                              FROM {user} u
    +                                             WHERE mnethostid = ?
    +                                               AND deleted = 1
    +                                               AND ".$DB->sql_isnotempty('user', 'email', false, false)."
    +                                               AND email = ?
    +                                               AND (
    +                                                       username LIKE ?
    +                                                    OR (
    +                                                           firstaccess != 0
    +                                                       AND firstaccess = ?
    +                                                       )
    +                                                   )",
    +                                           array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) {
    +                return $rec; // Matching user found, return it
    +            }
    +
    +            // 2B2 - If match by mnethost and user is deleted in DB and
    +            //       username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
    +            //       (this covers situations where md5(username) wasn't being stored so we require both
    +            //        the email & non-zero firstaccess to match)
    +            if ($rec = $DB->get_record_sql("SELECT *
    +                                              FROM {user} u
    +                                             WHERE mnethostid = ?
    +                                               AND deleted = 1
    +                                               AND username LIKE ?
    +                                               AND firstaccess != 0
    +                                               AND firstaccess = ?",
    +                                           array($user->mnethostid, $user->email.'.%', $user->firstaccess))) {
    +                return $rec; // Matching user found, return it
    +            }
     
                 // 2C - Handle users deleted in backup file and "alive" in DB
    +            // If match mnethost and user is deleted in backup file
    +            // and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
    +            if ($user->deleted) {
    +                // Note: for DB deleted users email is stored in username field, hence we
    +                //       are looking there for emails. See delete_user()
    +                // Trim time() from email
    +                $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
    +                if ($rec = $DB->get_record_sql("SELECT *
    +                                                  FROM {user} u
    +                                                 WHERE mnethostid = ?
    +                                                   AND email = ?
    +                                                   AND firstaccess != 0
    +                                                   AND firstaccess = ?",
    +                                               array($user->mnethostid, $trimemail, $user->firstaccess))) {
    +                    return $rec; // Matching user, deleted in backup file found, return it
    +                }
    +            }
     
    -            // 2D - If match by username and mnethost and not
    -            //     by (email or non-zero firstaccess) => conflict, return false
    +            // 2D - If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false
                 if ($rec = $DB->get_record_sql("SELECT *
                                                   FROM {user} u
                                                  WHERE username = ?
    @@ -830,7 +895,7 @@ function restore_check_user($restore, $user) {
             // Arrived here, return true as the user will need to be created and no
             // conflicts have been found in the logic above. This covers:
             // 1E - else => user needs to be created, return true
    -        // 2D - else => user needs to be created, return true
    +        // 2E - else => user needs to be created, return true
             return true;
         }
     
    
55f5b2e8b84e

MDL-16658 users pre-check. Committing current *disabled* code to have it

https://github.com/moodle/moodleEloy LafuenteNov 24, 2009via ghsa
1 file changed · +274 0
  • backup/restorelib.php+274 0 modified
    @@ -679,6 +679,262 @@ function restore_print_course_header ($course_header) {
             return $status;
         }
     
    +   /**
    +    * Given one user object (from backup file), perform all the neccesary
    +    * checks is order to decide how that user will be handled on restore.
    +    *
    +    * Note the function requires $user->mnethostid to be already calculated
    +    * so it's caller responsibility to set it
    +    *
    +    * This function is used both by @restore_precheck_users() and
    +    * @restore_create_users() to get consistent results in both places
    +    *
    +    * It returns:
    +    *   - one user object (from DB), if match has been found and user will be remapped
    +    *   - boolean true if the user needs to be created
    +    *   - boolean false if some conflict happened and the user cannot be handled
    +    *
    +    * Each test is responsible for returning its results and interrupt
    +    * execution. At the end, boolean true (user needs to be created) will be
    +    * returned if no test has interrupted that.
    +    *
    +    * Here it's the logic applied, keep it updated:
    +    *
    +    *  If restoring users from same site backup:
    +    *      1A - If match by id and username and mnethost  => ok, return target user
    +    *      1B - If match by id and mnethost and user is deleted in DB and
    +    *           match by email LIKE 'backup_email%'  => ok, return target user
    +    *      1C - If match by id and mnethost and user is deleted in backup file
    +    *           and match by email = email_without_time(backup_email) => ok, return target user
    +    *      1D - If match by username and mnethost and doesn't match by id => conflict, return false
    +    *      1E - else => user needs to be created, return true
    +    *
    +    *  if restoring from another site backup:
    +    *      2A - If match by username and mnethost and
    +    *           (email or non-zero firstaccess) => ok, return target user
    +    *      2B - Note: we cannot handle "deleted" situations here as far
    +    *           as username gets modified and id cannot be used here
    +    *      2C - If match by username and mnethost and not
    +    *           by (email or non-zero firstaccess) => conflict, return false
    +    *      2D - else => user needs to be created, return true
    +    */
    +    function restore_check_user($restore, $user) {
    +        global $CFG, $DB;
    +
    +        // Verify mnethostid is set, return error if not
    +        // it's parent responsibility to define that before
    +        // arriving here
    +        if (empty($user->mnethostid)) {
    +            debugging("restore_check_user() wrong use, mnethostid not set for user $user->username", DEBUG_DEVELOPER);
    +            return false;
    +        }
    +
    +        // Handle checks from same site backups
    +        if (backup_is_same_site($restore)) {
    +
    +            // 1A - If match by id and username and mnethost => ok, return target user
    +            if ($rec = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
    +                return $rec; // Matching user found, return it
    +            }
    +
    +            // 1B - Handle users deleted in DB and "alive" in backup file
    +            // 1B1- If match by id and mnethost and user is deleted in DB and
    +            //      match by email LIKE 'backup_email.%'  => ok, return target user
    +            // Note: for deleted users email is stored in username field, hence we
    +            //       are looking there for emails in the query below. See delete_user()
    +            if ($rec = $DB->get_record_sql("SELECT *
    +                                              FROM {user} u
    +                                             WHERE id = ?
    +                                               AND mnethostid = ?
    +                                               AND deleted = 1
    +                                               AND username LIKE ?",
    +                                           array($user->id, $user->mnethostid, $user->email.'.%')) {
    +                return $rec; // Matching user, deleted in DB found, return it
    +            }
    +
    +            // 1C - Handle users deleted in backup file and "alive" in DB
    +            // 1C1- If match by id and mnethost and user is deleted in backup file
    +            //      and match by email = email_without_time(backup_email) => ok, return target user
    +            if ($user->deleted) {
    +                // Trim time() from email
    +                // Note: for deleted users email is stored in username field, hece
    +                //       we are trimming the username field to get the email. See delete_user()
    +                $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
    +                if ($rec = $DB->get_record_sql("SELECT *
    +                                                  FROM {user} u
    +                                                 WHERE id = ?
    +                                                   AND mnethostid = ?
    +                                                   AND email = ?",
    +                                               array($user->id, $user->mnethostid, $trimemail))) {
    +                    return $rec; // Matching user, deleted in backup file found, return it
    +                }
    +            }
    +
    +            // 1D - If match by username and mnethost and doesn't match by id => conflict, return false
    +            if ($rec = $DB->get_record('user', array('username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
    +                if ($user->id != $rec->id) {
    +                    return false; // Conflict, username already exists and belongs to another id
    +                }
    +            }
    +
    +        // Handle checks from different site backups
    +        } else {
    +
    +            // 2A - If match by username and mnethost and
    +            //     (email or non-zero firstaccess) => ok, return target user
    +            if ($rec = $DB->get_record_sql("SELECT *
    +                                              FROM {user} u
    +                                             WHERE username = ?
    +                                               AND mnethostid = ?
    +                                               AND (
    +                                                       email = ?
    +                                                    OR (
    +                                                           firstaccess != 0
    +                                                       AND firstaccess = ?
    +                                                       )
    +                                                   )",
    +                                           array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
    +                return $rec; // Matching user
    +            }
    +
    +            // 2B - Handle users deleted in DB and "alive" in backup file
    +            // Note: for deleted users email is stored in username field, hence we
    +            //       are looking there for emails in the query below. See delete_user()
    +            // Note: for deleted users md5(username) is stored *sometimes* in the
    +            // email field, hence we are looking there for usernames in the query below
    +            // 2B - Note: we cannot handle "deleted" situations here as far
    +            //     as username gets modified and id cannot be used either
    +            // 2B1-deleted = 1 AND email = md5(username) AND mnsethostid AND (username like $user->email.% OR firstaccess)
    +            // 2B2 deleted and mnsethostid AND username like $user->email.% AND firstaccess
    +
    +            // 2C - Handle users deleted in backup file and "alive" in DB
    +
    +            // 2D - If match by username and mnethost and not
    +            //     by (email or non-zero firstaccess) => conflict, return false
    +            if ($rec = $DB->get_record_sql("SELECT *
    +                                              FROM {user} u
    +                                             WHERE username = ?
    +                                               AND mnethostid = ?
    +                                           AND NOT (
    +                                                       email = ?
    +                                                    OR (
    +                                                           firstaccess != 0
    +                                                       AND firstaccess = ?
    +                                                       )
    +                                                   )",
    +                                           array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
    +                return false; // Conflict, username/mnethostid already exist and belong to another user (by email/firstaccess)
    +            }
    +        }
    +
    +        // Arrived here, return true as the user will need to be created and no
    +        // conflicts have been found in the logic above. This covers:
    +        // 1E - else => user needs to be created, return true
    +        // 2D - else => user needs to be created, return true
    +        return true;
    +    }
    +
    +   /**
    +    * For all the users being restored, check if they are going to cause problems
    +    * before executing the restore process itself, detecting situations like:
    +    *   - conflicts preventing restore to continue - provided by @restore_check_user()
    +    *   - prevent creation of users if not allowed - check some global settings/caps
    +    */
    +    function restore_precheck_users($xml_file, $restore, &$problems) {
    +        global $CFG, $DB;
    +
    +        $status = true; // Init $status
    +
    +        // We aren't restoring users, nothing to check, allow continue
    +        if ($restore->users == 2) {
    +            return true;
    +        }
    +
    +        // Get array of users from xml file and load them in backup_ids table
    +        if (!$info = restore_read_xml_users($restore,$xml_file)) {
    +            return true; // No users, nothing to check, allow continue
    +        }
    +
    +        // We are going to map mnethostid, so load all the available ones
    +        $mnethosts = $DB->get_records('mnet_host', array(), 'wwwroot', 'wwwroot, id');
    +
    +        // Calculate the context we are going to use for capability checking
    +        if (!empty($restore->course_id)) { // Know the target (existing) course, check capabilities there
    +            $context = get_context_instance(CONTEXT_COURSE, $restore->course_id);
    +        } else if (!empty($restore->restore_restorecatto)) { // Know the category, check capabilities there
    +            $context = get_context_instance(CONTEXT_COURSECAT, $restore->restore_restorecatto);
    +        } else { // Last resort, check capabilities at system level
    +            $context = get_context_instance(CONTEXT_SYSTEM);
    +        }
    +
    +        // Calculate if we have perms to create users, by checking:
    +        // to 'moodle/restore:createuser' and 'moodle/restore:userinfo'
    +        // and also observe $CFG->disableusercreationonrestore
    +        $cancreateuser = false;
    +        if (has_capability('moodle/restore:createuser', $context) and
    +            has_capability('moodle/restore:userinfo', $context) and
    +            empty($CFG->disableusercreationonrestore)) { // Can create users
    +
    +            $cancreateuser = true;
    +        }
    +
    +        // Iterate over all users, checking if they are likely to cause problems on restore
    +        $counter = 0;
    +        foreach ($info->users as $userid) {
    +            $rec = backup_getid($restore->backup_unique_code, 'user', $userid);
    +            $user = $rec->info;
    +
    +            // Find the correct mnethostid for user before performing any further check
    +            if (empty($user->mnethosturl) || $user->mnethosturl===$CFG->wwwroot) {
    +                $user->mnethostid = $CFG->mnet_localhost_id;
    +            } else {
    +                // fast url-to-id lookups
    +                if (isset($mnethosts[$user->mnethosturl])) {
    +                    $user->mnethostid = $mnethosts[$user->mnethosturl]->id;
    +                } else {
    +                    $user->mnethostid = $CFG->mnet_localhost_id;
    +                }
    +            }
    +
    +            // Calculate the best way to handle this user from backup file
    +            $usercheck = restore_check_user($restore, $user);
    +
    +            if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to
    +
    +            } else if ($usercheck === false) { // Found conflict, report it as problem
    +                $problems[] = get_string('restoreuserconflict', '', $user->username);
    +                $status = false;
    +
    +            } else if ($usercheck === true) { // User needs to be created, check if we are able
    +                if (!$cancreateuser) { // Cannot create, report as problem
    +
    +                    $problems[] = get_string('restorecannotcreateuser', '', $user->username);
    +                    $status = false;
    +                }
    +
    +            } else { // Shouldn't arrive here ever, something is for sure wrong in restore_check_user()
    +                if (!defined('RESTORE_SILENTLY')) {
    +                    notify('Unexpected error pre-checking user ' . s($user->username) . ' from backup file');
    +                    return false;
    +                }
    +            }
    +
    +            // Do some output
    +            $counter++;
    +            if ($counter % 10 == 0) {
    +                if (!defined('RESTORE_SILENTLY')) {
    +                    echo ".";
    +                    if ($counter % 200 == 0) {
    +                        echo "<br />";
    +                    }
    +                }
    +                backup_flush(300);
    +            }
    +        }
    +
    +        return $status;
    +    }
    +
         //This function create a new course record.
         //When finished, course_header contains the id of the new course
         function restore_create_new_course($restore,&$course_header) {
    @@ -8164,6 +8420,24 @@ function restore_execute(&$restore,$info,$course_header,&$errorstr) {
                 }
             }
     
    +        // Precheck the users section, detecting various situations that can lead to problems, so
    +        // we stop restore before performing any further action
    +        /*
    +        if (!defined('RESTORE_SILENTLY')) {
    +            echo '<li>'.get_string('restoreusersprecheck').'</li>';
    +        }
    +        if (!restore_precheck_users($xml_file, $restore, $problems)) {
    +            $errorstr = get_string('restoreusersprecheckerror');
    +            if (!empty($problems)) {
    +                $errorstr .= ' (' . implode(', ', $problems)  . ')';
    +            }
    +            if (!defined('RESTORE_SILENTLY')) {
    +                notify($errorstr);
    +            }
    +            return false;
    +        }
    +        */
    +
             //If we've selected to restore into new course
             //create it (course)
             //Saving conversion id variables into backup_tables
    

Vulnerability mechanics

Root cause

"Missing capability check during course restore allows teachers to create new user accounts without the moodle/user:create capability."

Attack vector

An attacker with teacher-level access to a Moodle course can exploit this vulnerability by restoring a course backup that contains user data. During the restore process, Moodle 1.8.x and 1.9.x before 1.9.8 would create new user accounts from the backup file without verifying that the restoring user holds the moodle/user:create capability. The attacker only needs the ability to restore courses (typically granted to teachers) and a backup file containing user records. No special network position is required beyond normal authenticated access to the Moodle site.

Affected code

The vulnerability exists in backup/restorelib.php, specifically in the restore_create_users() function which previously created users from backup XML without any capability check. The patch adds restore_precheck_users() and restore_check_user() functions to the same file [patch_id=19447], and modifies restore_create_users() to use pre-computed results from backup_ids [patch_id=19449]. A new capability 'moodle/restore:createuser' is defined in lib/db/access.php [patch_id=19449].

What the fix does

The patch introduces a new capability 'moodle/restore:createuser' defined in lib/db/access.php [patch_id=19449] and adds a pre-check function restore_precheck_users() in backup/restorelib.php [patch_id=19447]. Before executing the restore, the pre-check iterates over all users in the backup file and calls restore_check_user() to determine whether each user needs to be created. If creation is needed, it verifies that the restoring user has both 'moodle/restore:createuser' and 'moodle/restore:userinfo' capabilities and that the $CFG->disableusercreationonrestore setting is not enabled. If the user lacks these capabilities, the restore is halted with an error message [patch_id=19449]. Additionally, restore_create_users() was refactored to rely on the pre-check results stored in backup_ids rather than re-parsing the XML and re-checking permissions [patch_id=19449].

Preconditions

  • authAttacker must have a role (e.g., teacher) that allows restoring courses
  • inputAttacker must have access to a course backup file containing user records
  • configThe Moodle version must be 1.8.x or 1.9.x before 1.9.8

Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.