CVE-2022-0731: Authorization Bypass Through User-Controlled Key (CWE-639)
value: Improper Access Control (IDOR) in GitHub repository dolibarr/dolibarr prior to 16.0.
CWE description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
CVSS v3 base score: 6.5
Published: 2022-02-23T19:15Z
Repository: https://github.com/dolibarr/dolibarr
Language: PHP
Files changed: htdocs/core/class/html.form.class.php, htdocs/core/lib/files.lib.php, htdocs/install/upgrade2.php, htdocs/user/card.php
Commit date: 2022-02-22 21:24:34 +0100
Commit message: FIX #hunterb812ea22-0c02-46fe-b89f-04519dfb1ebd
Diff: diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php
index fa7fb264995..68fe5152407 100644
--- a/htdocs/core/class/html.form.class.php
+++ b/htdocs/core/class/html.form.class.php
@@ -9093,17 +9093,17 @@ class Form
 		} elseif ($modulepart == 'userphoto') {
 			$dir = $conf->user->dir_output;
 			if (!empty($object->photo)) {
 				if (dolIsAllowedForPreview($object->photo)) {
 					if ((string) $imagesize == 'mini') {
-						$file = get_exdir(0, 0, 0, 0, $object, 'user').getImageFileNameForSize($object->photo, '_mini');
+						$file = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.getImageFileNameForSize($object->photo, '_mini');
 					} elseif ((string) $imagesize == 'small') {
-						$file = get_exdir(0, 0, 0, 0, $object, 'user').getImageFileNameForSize($object->photo, '_small');
+						$file = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.getImageFileNameForSize($object->photo, '_small');
 					} else {
-						$file = get_exdir(0, 0, 0, 0, $object, 'user').$object->photo;
+						$file = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.$object->photo;
 					}
-					$originalfile = get_exdir(0, 0, 0, 0, $object, 'user').$object->photo;
+					$originalfile = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.$object->photo;
 				}
 			}
 			if (!empty($conf->global->MAIN_OLD_IMAGE_LINKS)) {
 				$altfile = $object->id.".jpg"; // For backward compatibility
 			}
diff --git a/htdocs/core/lib/files.lib.php b/htdocs/core/lib/files.lib.php
index c70ea041e0a..0a4455239bb 100644
--- a/htdocs/core/lib/files.lib.php
+++ b/htdocs/core/lib/files.lib.php
@@ -2346,19 +2346,19 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 	} elseif ($modulepart == 'logs' && !empty($dolibarr_main_data_root)) {
 		// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
 		$accessallowed = ($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.log$/', basename($original_file)));
 		$original_file = $dolibarr_main_data_root.'/'.$original_file;
 	} elseif ($modulepart == 'doctemplates' && !empty($dolibarr_main_data_root)) {
-		// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
+		// Wrapping for doctemplates
 		$accessallowed = $user->admin;
 		$original_file = $dolibarr_main_data_root.'/doctemplates/'.$original_file;
 	} elseif ($modulepart == 'doctemplateswebsite' && !empty($dolibarr_main_data_root)) {
-		// Wrapping for *.zip files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
+		// Wrapping for doctemplates of websites
 		$accessallowed = ($fuser->rights->website->write && preg_match('/\.jpg$/i', basename($original_file)));
 		$original_file = $dolibarr_main_data_root.'/doctemplates/websites/'.$original_file;
 	} elseif ($modulepart == 'packages' && !empty($dolibarr_main_data_root)) {
-		// Wrapping for *.zip files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
+		// Wrapping for *.zip package files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
 		// Dir for custom dirs
 		$tmp = explode(',', $dolibarr_main_document_root_alt);
 		$dirins = $tmp[0];
 
 		$accessallowed = ($user->admin && preg_match('/^module_.*\.zip$/', basename($original_file)));
@@ -2367,15 +2367,21 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 		// Wrapping for some images
 		$accessallowed = 1;
 		$original_file = $conf->mycompany->dir_output.'/'.$original_file;
 	} elseif ($modulepart == 'userphoto' && !empty($conf->user->dir_output)) {
 		// Wrapping for users photos
-		$accessallowed = 1;
+		$accessallowed = 0;
+		if (preg_match('/^\d+\/photos\//', $original_file)) {
+			$accessallowed = 1;
+		}
 		$original_file = $conf->user->dir_output.'/'.$original_file;
 	} elseif ($modulepart == 'memberphoto' && !empty($conf->adherent->dir_output)) {
 		// Wrapping for members photos
-		$accessallowed = 1;
+		$accessallowed = 0;
+		if (preg_match('/^\d+\/photos\//', $original_file)) {
+			$accessallowed = 1;
+		}
 		$original_file = $conf->adherent->dir_output.'/'.$original_file;
 	} elseif ($modulepart == 'apercufacture' && !empty($conf->facture->multidir_output[$entity])) {
 		// Wrapping pour les apercu factures
 		if ($fuser->rights->facture->{$lire}) {
 			$accessallowed = 1;
@@ -2446,11 +2452,11 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 				$accessallowed = checkUserAccessToObject($user, array('expensereport'), $tmpexpensereport, 'expensereport', '', '', 'rowid', '');
 			}
 		}
 		$original_file = $conf->expensereport->dir_output.'/'.$original_file;
 	} elseif (($modulepart == 'apercuexpensereport') && !empty($conf->expensereport->dir_output)) {
-		// Wrapping pour les apercu supplier invoice
+		// Wrapping pour les apercu expense report
 		if ($fuser->rights->expensereport->{$lire}) {
 			$accessallowed = 1;
 		}
 		$original_file = $conf->expensereport->dir_output.'/'.$original_file;
 	} elseif ($modulepart == 'propalstats' && !empty($conf->propal->multidir_temp[$entity])) {
diff --git a/htdocs/install/upgrade2.php b/htdocs/install/upgrade2.php
index 9be3cc361a0..924d251702c 100644
--- a/htdocs/install/upgrade2.php
+++ b/htdocs/install/upgrade2.php
@@ -454,10 +454,17 @@ if (!GETPOST('action', 'aZ09') || preg_match('/upgrade/i', GETPOST('action', 'aZ
 		$beforeversionarray = explode('.', '14.0.9');
 		if (versioncompare($versiontoarray, $afterversionarray) >= 0 && versioncompare($versiontoarray, $beforeversionarray) <= 0) {
 			migrate_export_import_profiles('export');
 			migrate_export_import_profiles('import');
 		}
+
+		// Scripts for 16.0
+		$afterversionarray = explode('.', '15.0.9');
+		$beforeversionarray = explode('.', '16.0.9');
+		if (versioncompare($versiontoarray, $afterversionarray) >= 0 && versioncompare($versiontoarray, $beforeversionarray) <= 0) {
+			migrate_user_photospath2();
+		}
 	}
 
 
 	// Code executed only if migration is LAST ONE. Must always be done.
 	if (versioncompare($versiontoarray, $versionranarray) >= 0 || versioncompare($versiontoarray, $versionranarray) <= -3) {
@@ -4503,12 +4510,11 @@ function migrate_user_photospath()
 											//var_dump('aaa');exit;
 										}
 									}
 									// dol_delete_dir($origin.'/'.$file);
 								}
-							} else // it is a file
-							{
+							} else { // it is a file
 								if (!dol_is_file($destin.'/'.$file)) {
 									//print $origin.'/'.$file.' -> '.$destin.'/'.$file.'<br>'."\n";
 									print '.';
 									dol_copy($origin.'/'.$file, $destin.'/'.$file, 0, 0);
 									//var_dump('eee');exit;
@@ -4522,10 +4528,98 @@ function migrate_user_photospath()
 	}
 
 	print '</td></tr>';
 }
 
+/**
+ * Migrate file from old path users/99/file.jpg into users/99/photos/file.jpg
+ *
+ * @return	void
+ */
+function migrate_user_photospath2()
+{
+	global $conf, $db, $langs, $user;
+
+	print '<tr><td colspan="4">';
+
+	print '<b>'.$langs->trans('MigrationUserPhotoPath')."</b><br>\n";
+
+	include_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
+	$fuser = new User($db);
+
+	if (!is_object($user)) {
+		$user = $fuser; // To avoid error during migration
+	}
+
+	$sql = "SELECT rowid as uid from ".MAIN_DB_PREFIX."user"; // Get list of all users
+	$resql = $db->query($sql);
+	if ($resql) {
+		while ($obj = $db->fetch_object($resql)) {
+			$fuser->fetch($obj->uid);
+			//echo '<hr>'.$fuser->id.' -> '.$fuser->entity;
+			$entity = (empty($fuser->entity) ? 1 : $fuser->entity);
+			if ($entity > 1) {
+				$dir = DOL_DATA_ROOT.'/'.$entity.'/users';
+			} else {
+				$dir = $conf->user->multidir_output[$entity]; // $conf->user->multidir_output[] for each entity is construct by the multicompany module
+			}
+
+			if ($dir) {
+				//print "Process user id ".$fuser->id."<br>\n";
+				$origin = $dir.'/'.$fuser->id;
+				$destin = $dir.'/'.$fuser->id.'/photos';
+
+				$origin_osencoded = dol_osencode($origin);
+
+				dol_mkdir($destin);
+
+				//echo '<hr>'.$origin.' -> '.$destin;
+				if (dol_is_dir($origin)) {
+					$handle = opendir($origin_osencoded);
+					if (is_resource($handle)) {
+						while (($file = readdir($handle)) !== false) {
+							if ($file == '.' || $file == '..' || $file == 'photos') {
+								continue;
+							}
+							if (!empty($fuser->photo) && ($file != $fuser->photo && $file != 'thumbs')) {
+								continue;
+							}
+
+							if (dol_is_dir($origin.'/'.$file)) {	// it is a dir (like 'thumbs')
+								$thumbs = opendir($origin_osencoded.'/'.$file);
+								if (is_resource($thumbs)) {
+									dol_mkdir($destin.'/'.$file);
+									while (($thumb = readdir($thumbs)) !== false) {
+										if (!dol_is_file($destin.'/'.$file.'/'.$thumb)) {
+											if ($thumb == '.' || $thumb == '..') {
+												continue;
+											}
+
+											//print $origin.'/'.$file.'/'.$thumb.' -> '.$destin.'/'.$file.'/'.$thumb.'<br>'."\n";
+											print '.';
+											dol_copy($origin.'/'.$file.'/'.$thumb, $destin.'/'.$file.'/'.$thumb, 0, 0);
+										}
+									}
+									// dol_delete_dir($origin.'/'.$file);
+								}
+							} else { // it is a file
+								if (!dol_is_file($destin.'/'.$file)) {
+									//print $origin.'/'.$file.' -> '.$destin.'/'.$file.'<br>'."\n";
+									print '.';
+									dol_copy($origin.'/'.$file, $destin.'/'.$file, 0, 0);
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	print '</td></tr>';
+}
+
 
 /* A faire egalement: Modif statut paye et fk_facture des factures payes completement
 
 On recherche facture incorrecte:
 select f.rowid, f.total_ttc as t1, sum(pf.amount) as t2 from llx_facture as f, llx_paiement_facture as pf where pf.fk_facture=f.rowid and f.fk_statut in(2,3) and paye=0 and close_code is null group by f.rowid
diff --git a/htdocs/user/card.php b/htdocs/user/card.php
index c5103278dda..f9b92d36d72 100644
--- a/htdocs/user/card.php
+++ b/htdocs/user/card.php
@@ -541,18 +541,18 @@ if (empty($reshook)) {
 					}
 				}
 
 				if (!$error && !count($object->errors)) {
 					if (GETPOST('deletephoto') && $object->oldcopy->photo) {
-						$fileimg = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').$object->oldcopy->photo;
-						$dirthumbs = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').'/thumbs';
+						$fileimg = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.$object->oldcopy->photo;
+						$dirthumbs = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').'photos/thumbs';
 						dol_delete_file($fileimg);
 						dol_delete_dir_recursive($dirthumbs);
 					}
 
 					if (isset($_FILES['photo']['tmp_name']) && trim($_FILES['photo']['tmp_name'])) {
-						$dir = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 1, $object, 'user');
+						$dir = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 1, $object, 'user').'/photos';
 
 						dol_mkdir($dir);
 
 						if (@is_dir($dir)) {
 							$newfile = $dir.'/'.dol_sanitizeFileName($_FILES['photo']['name']);

Vulnerable code: 						$file = get_exdir(0, 0, 0, 0, $object, 'user').getImageFileNameForSize($object->photo, '_mini');
						$file = get_exdir(0, 0, 0, 0, $object, 'user').getImageFileNameForSize($object->photo, '_small');
						$file = get_exdir(0, 0, 0, 0, $object, 'user').$object->photo;
					$originalfile = get_exdir(0, 0, 0, 0, $object, 'user').$object->photo;
		// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
		// Wrapping for *.zip files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
		// Wrapping for *.zip files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
		$accessallowed = 1;
		$accessallowed = 1;
		// Wrapping pour les apercu supplier invoice
							} else // it is a file
							{
						$fileimg = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').$object->oldcopy->photo;
						$dirthumbs = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').'/thumbs';
						$dir = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 1, $object, 'user');
Fixed code: 						$file = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.getImageFileNameForSize($object->photo, '_mini');
						$file = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.getImageFileNameForSize($object->photo, '_small');
						$file = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.$object->photo;
					$originalfile = get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.$object->photo;
		// Wrapping for doctemplates
		// Wrapping for doctemplates of websites
		// Wrapping for *.zip package files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
		$accessallowed = 0;
		if (preg_match('/^\d+\/photos\//', $original_file)) {
			$accessallowed = 1;
		}
		$accessallowed = 0;
		if (preg_match('/^\d+\/photos\//', $original_file)) {
			$accessallowed = 1;
		}
		// Wrapping pour les apercu expense report

		// Scripts for 16.0
		$afterversionarray = explode('.', '15.0.9');
		$beforeversionarray = explode('.', '16.0.9');
		if (versioncompare($versiontoarray, $afterversionarray) >= 0 && versioncompare($versiontoarray, $beforeversionarray) <= 0) {
			migrate_user_photospath2();
		}
							} else { // it is a file
/**
 * Migrate file from old path users/99/file.jpg into users/99/photos/file.jpg
 *
 * @return	void
 */
function migrate_user_photospath2()
{
	global $conf, $db, $langs, $user;

	print '<tr><td colspan="4">';

	print '<b>'.$langs->trans('MigrationUserPhotoPath')."</b><br>\n";

	include_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
	$fuser = new User($db);

	if (!is_object($user)) {
		$user = $fuser; // To avoid error during migration
	}

	$sql = "SELECT rowid as uid from ".MAIN_DB_PREFIX."user"; // Get list of all users
	$resql = $db->query($sql);
	if ($resql) {
		while ($obj = $db->fetch_object($resql)) {
			$fuser->fetch($obj->uid);
			//echo '<hr>'.$fuser->id.' -> '.$fuser->entity;
			$entity = (empty($fuser->entity) ? 1 : $fuser->entity);
			if ($entity > 1) {
				$dir = DOL_DATA_ROOT.'/'.$entity.'/users';
			} else {
				$dir = $conf->user->multidir_output[$entity]; // $conf->user->multidir_output[] for each entity is construct by the multicompany module
			}

			if ($dir) {
				//print "Process user id ".$fuser->id."<br>\n";
				$origin = $dir.'/'.$fuser->id;
				$destin = $dir.'/'.$fuser->id.'/photos';

				$origin_osencoded = dol_osencode($origin);

				dol_mkdir($destin);

				//echo '<hr>'.$origin.' -> '.$destin;
				if (dol_is_dir($origin)) {
					$handle = opendir($origin_osencoded);
					if (is_resource($handle)) {
						while (($file = readdir($handle)) !== false) {
							if ($file == '.' || $file == '..' || $file == 'photos') {
								continue;
							}
							if (!empty($fuser->photo) && ($file != $fuser->photo && $file != 'thumbs')) {
								continue;
							}

							if (dol_is_dir($origin.'/'.$file)) {	// it is a dir (like 'thumbs')
								$thumbs = opendir($origin_osencoded.'/'.$file);
								if (is_resource($thumbs)) {
									dol_mkdir($destin.'/'.$file);
									while (($thumb = readdir($thumbs)) !== false) {
										if (!dol_is_file($destin.'/'.$file.'/'.$thumb)) {
											if ($thumb == '.' || $thumb == '..') {
												continue;
											}

											//print $origin.'/'.$file.'/'.$thumb.' -> '.$destin.'/'.$file.'/'.$thumb.'<br>'."\n";
											print '.';
											dol_copy($origin.'/'.$file.'/'.$thumb, $destin.'/'.$file.'/'.$thumb, 0, 0);
										}
									}
									// dol_delete_dir($origin.'/'.$file);
								}
							} else { // it is a file
								if (!dol_is_file($destin.'/'.$file)) {
									//print $origin.'/'.$file.' -> '.$destin.'/'.$file.'<br>'."\n";
									print '.';
									dol_copy($origin.'/'.$file, $destin.'/'.$file, 0, 0);
								}
							}
						}
					}
				}
			}
		}
	}

	print '</td></tr>';
}

						$fileimg = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').'photos/'.$object->oldcopy->photo;
						$dirthumbs = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, 'user').'photos/thumbs';
						$dir = $conf->user->dir_output.'/'.get_exdir(0, 0, 0, 1, $object, 'user').'/photos';