CVE-2023-2034: Unrestricted Upload of File with Dangerous Type (CWE-434)
value: Unrestricted Upload of File with Dangerous Type in GitHub repository froxlor/froxlor prior to 2.0.14.
CWE description: The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.
CVSS v3 base score: 8.8
Published: 2023-04-14T01:15Z
Repository: https://github.com/froxlor/froxlor
Language: PHP
Files changed: lib/Froxlor/SImExporter.php, lib/Froxlor/Settings/Store.php, lib/Froxlor/Validate/Validate.php
Commit date: 2023-03-08 09:33:30 +0100
Commit message: better validation for uploaded/imported image files

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>

Diff: diff --git a/lib/Froxlor/SImExporter.php b/lib/Froxlor/SImExporter.php
index 6cb049b4d2..a40e24bb7f 100644
--- a/lib/Froxlor/SImExporter.php
+++ b/lib/Froxlor/SImExporter.php
@@ -26,10 +26,11 @@
 namespace Froxlor;
 
 use Exception;
 use Froxlor\Database\Database;
 use Froxlor\UI\Form;
+use Froxlor\Validate\Validate;
 use PDO;
 
 /**
  * Class SImExporter
  *
@@ -157,10 +158,13 @@ class SImExporter
 				}
 			}
 			// re-format the array-key for Form::processForm
 			foreach ($_data as $key => $value) {
 				$index_split = explode('.', $key, 3);
+				if (!isset($current_settings[$index_split[0]][$index_split[1]])) {
+					continue;
+				}
 				if (isset($index_split[2]) && $index_split[2] === 'image_data' && !empty($_data[$index_split[0] . '.' . $index_split[1]])) {
 					$image_data[$key] = $value;
 				} else {
 					$form_data[str_replace(".", "_", $key)] = $value;
 				}
@@ -188,46 +192,31 @@ class SImExporter
 							if (!chmod($path, 0775)) {
 								throw new Exception("Cannot write to img directory");
 							}
 						}
 
-						$img_data = base64_decode($value);
-						$img_filename = Froxlor::getInstallDir() . '/' . str_replace('../', '',
-								explode('?', $_data[$index_split[0] . '.' . $index_split[1]], 2)[0]);
-
-						file_put_contents($img_filename, $img_data);
-
-						if (function_exists('finfo_open')) {
-							$finfo = finfo_open(FILEINFO_MIME_TYPE);
-							$mimetype = finfo_file($finfo, $img_filename);
-							finfo_close($finfo);
-						} else {
-							$mimetype = mime_content_type($img_filename);
-						}
-						if (empty($mimetype)) {
-							$mimetype = 'application/octet-stream';
-						}
-						if (!in_array($mimetype, ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])) {
-							@unlink($img_filename);
-							throw new Exception("Uploaded file is not a valid image");
-						}
-
-						$spl = explode('.', $img_filename);
-						$file_extension = strtolower(array_pop($spl));
-						unset($spl);
-
-						if (!in_array($file_extension, [
-							'jpeg',
-							'jpg',
-							'png',
-							'gif'
-						])) {
-							@unlink($img_filename);
-							throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
+						if (Validate::validateBase64Image($value)) {
+							$img_data = base64_decode($value);
+							$img_filename = explode('?', $_data[$index_split[0] . '.' . $index_split[1]], 2)[0];
+
+							$spl = explode('.', $img_filename);
+							$file_extension = strtolower(array_pop($spl));
+							unset($spl);
+
+							if (!in_array($file_extension, [
+								'jpeg',
+								'jpg',
+								'png',
+								'gif'
+							])) {
+								throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
+							}
+							$img_filename = 'img/' . bin2hex(random_bytes(16)) . '.' . $file_extension;
+							file_put_contents(Froxlor::getInstallDir() . '/' . $img_filename, $img_data);
+							$img_index = $index_split[0].'.'.$index_split[1];
+							Settings::Set($img_index, $img_filename . '?v=' . time());
 						}
-
-						Settings::Set($index, $value);
 					}
 				}
 				// all good
 				return true;
 			} else {
diff --git a/lib/Froxlor/Settings/Store.php b/lib/Froxlor/Settings/Store.php
index 451df1aa1f..8c949d7945 100644
--- a/lib/Froxlor/Settings/Store.php
+++ b/lib/Froxlor/Settings/Store.php
@@ -34,10 +34,11 @@ use Froxlor\Froxlor;
 use Froxlor\Idna\IdnaWrapper;
 use Froxlor\PhpHelper;
 use Froxlor\Settings;
 use Froxlor\System\Cronjob;
 use Froxlor\System\IPTools;
+use Froxlor\Validate\Validate;
 use PDO;
 
 class Store
 {
 
@@ -413,44 +414,34 @@ class Store
 						throw new Exception("Cannot write to img directory");
 					}
 				}
 
 				// Make sure mime-type matches an image
-				if (function_exists('finfo_open')) {
-					$finfo = finfo_open(FILEINFO_MIME_TYPE);
-					$mimetype = finfo_file($finfo, $_FILES[$fieldname]['tmp_name']);
-					finfo_close($finfo);
-				} else {
-					$mimetype = mime_content_type($_FILES[$fieldname]['tmp_name']);
-				}
-				if (empty($mimetype)) {
-					$mimetype = 'application/octet-stream';
-				}
-				if (!in_array($mimetype, ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])) {
-					throw new \Exception("Uploaded file is not a valid image");
-				}
-
-				// Determine file extension
-				$spl = explode('.', $_FILES[$fieldname]['name']);
-				$file_extension = strtolower(array_pop($spl));
-				unset($spl);
-
-				if (!in_array($file_extension, [
-					'jpeg',
-					'jpg',
-					'png',
-					'gif'
-				])) {
-					throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
-				}
-
-				// Move file
-				if (!move_uploaded_file($_FILES[$fieldname]['tmp_name'], $path . $fielddata['image_name'] . '.' . $file_extension)) {
-					throw new Exception("Unable to save image to img folder");
+				$image_content = file_get_contents($_FILES[$fieldname]['tmp_name']);
+				$value = base64_encode($image_content);
+				if (Validate::validateBase64Image($value)) {
+					$img_filename = $_FILES[$fieldname]['name'];
+
+					$spl = explode('.', $img_filename);
+					$file_extension = strtolower(array_pop($spl));
+					unset($spl);
+
+					if (!in_array($file_extension, [
+						'jpeg',
+						'jpg',
+						'png',
+						'gif'
+					])) {
+						throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
+					}
+					$filename = bin2hex(random_bytes(16)) . '.' . $file_extension;
+					// Move file
+					if (!move_uploaded_file($_FILES[$fieldname]['tmp_name'], $path . $filename)) {
+						throw new Exception("Unable to save image to img folder");
+					}
+					$save_to = 'img/' . $filename . '?v=' . time();
 				}
-
-				$save_to = 'img/' . $fielddata['image_name'] . '.' . $file_extension . '?v=' . time();
 			}
 
 			// Delete file?
 			if ($fielddata['value'] !== "" && array_key_exists($fieldname . '_delete', $_POST) && $_POST[$fieldname . '_delete']) {
 				@unlink(Froxlor::getInstallDir() . '/' . explode('?', $fielddata['value'], 2)[0]);
diff --git a/lib/Froxlor/Validate/Validate.php b/lib/Froxlor/Validate/Validate.php
index a3d83ca548..ae57e5ab6f 100644
--- a/lib/Froxlor/Validate/Validate.php
+++ b/lib/Froxlor/Validate/Validate.php
@@ -332,6 +332,42 @@ class Validate
 				return true;
 			}
 		}
 		return false;
 	}
+
+	/**
+	 * validates whether a given base64 string decodes to an image
+	 *
+	 * @param string $base64string
+	 * @return bool
+	 * @throws Exception
+	 */
+	public static function validateBase64Image(string $base64string) {
+
+		if (!extension_loaded('gd')) {
+			Response::standardError('phpgdextensionnotavailable', null, true);
+		}
+
+		// Decode the base64 string
+		$data = base64_decode($base64string);
+
+		// Create an image from the decoded data
+		$image = @imagecreatefromstring($data);
+
+		// Check if the image was created successfully
+		if (!$image) {
+			return false;
+		}
+
+		// Get the MIME type of the image
+		$mime = image_type_to_mime_type(getimagesizefromstring($data)[2]);
+
+		// Check if the MIME type is a valid image MIME type
+		if (strpos($mime, 'image/') !== 0) {
+			return false;
+		}
+
+		// If everything is okay, return true
+		return true;
+	}
 }

Vulnerable code: 						$img_data = base64_decode($value);
						$img_filename = Froxlor::getInstallDir() . '/' . str_replace('../', '',
								explode('?', $_data[$index_split[0] . '.' . $index_split[1]], 2)[0]);

						file_put_contents($img_filename, $img_data);

						if (function_exists('finfo_open')) {
							$finfo = finfo_open(FILEINFO_MIME_TYPE);
							$mimetype = finfo_file($finfo, $img_filename);
							finfo_close($finfo);
						} else {
							$mimetype = mime_content_type($img_filename);
						}
						if (empty($mimetype)) {
							$mimetype = 'application/octet-stream';
						}
						if (!in_array($mimetype, ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])) {
							@unlink($img_filename);
							throw new Exception("Uploaded file is not a valid image");
						}

						$spl = explode('.', $img_filename);
						$file_extension = strtolower(array_pop($spl));
						unset($spl);

						if (!in_array($file_extension, [
							'jpeg',
							'jpg',
							'png',
							'gif'
						])) {
							@unlink($img_filename);
							throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");

						Settings::Set($index, $value);
				if (function_exists('finfo_open')) {
					$finfo = finfo_open(FILEINFO_MIME_TYPE);
					$mimetype = finfo_file($finfo, $_FILES[$fieldname]['tmp_name']);
					finfo_close($finfo);
				} else {
					$mimetype = mime_content_type($_FILES[$fieldname]['tmp_name']);
				}
				if (empty($mimetype)) {
					$mimetype = 'application/octet-stream';
				}
				if (!in_array($mimetype, ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])) {
					throw new \Exception("Uploaded file is not a valid image");
				}

				// Determine file extension
				$spl = explode('.', $_FILES[$fieldname]['name']);
				$file_extension = strtolower(array_pop($spl));
				unset($spl);

				if (!in_array($file_extension, [
					'jpeg',
					'jpg',
					'png',
					'gif'
				])) {
					throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
				}

				// Move file
				if (!move_uploaded_file($_FILES[$fieldname]['tmp_name'], $path . $fielddata['image_name'] . '.' . $file_extension)) {
					throw new Exception("Unable to save image to img folder");

				$save_to = 'img/' . $fielddata['image_name'] . '.' . $file_extension . '?v=' . time();
Fixed code: use Froxlor\Validate\Validate;
				if (!isset($current_settings[$index_split[0]][$index_split[1]])) {
					continue;
				}
						if (Validate::validateBase64Image($value)) {
							$img_data = base64_decode($value);
							$img_filename = explode('?', $_data[$index_split[0] . '.' . $index_split[1]], 2)[0];

							$spl = explode('.', $img_filename);
							$file_extension = strtolower(array_pop($spl));
							unset($spl);

							if (!in_array($file_extension, [
								'jpeg',
								'jpg',
								'png',
								'gif'
							])) {
								throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
							}
							$img_filename = 'img/' . bin2hex(random_bytes(16)) . '.' . $file_extension;
							file_put_contents(Froxlor::getInstallDir() . '/' . $img_filename, $img_data);
							$img_index = $index_split[0].'.'.$index_split[1];
							Settings::Set($img_index, $img_filename . '?v=' . time());
use Froxlor\Validate\Validate;
				$image_content = file_get_contents($_FILES[$fieldname]['tmp_name']);
				$value = base64_encode($image_content);
				if (Validate::validateBase64Image($value)) {
					$img_filename = $_FILES[$fieldname]['name'];

					$spl = explode('.', $img_filename);
					$file_extension = strtolower(array_pop($spl));
					unset($spl);

					if (!in_array($file_extension, [
						'jpeg',
						'jpg',
						'png',
						'gif'
					])) {
						throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif");
					}
					$filename = bin2hex(random_bytes(16)) . '.' . $file_extension;
					// Move file
					if (!move_uploaded_file($_FILES[$fieldname]['tmp_name'], $path . $filename)) {
						throw new Exception("Unable to save image to img folder");
					}
					$save_to = 'img/' . $filename . '?v=' . time();

	/**
	 * validates whether a given base64 string decodes to an image
	 *
	 * @param string $base64string
	 * @return bool
	 * @throws Exception
	 */
	public static function validateBase64Image(string $base64string) {

		if (!extension_loaded('gd')) {
			Response::standardError('phpgdextensionnotavailable', null, true);
		}

		// Decode the base64 string
		$data = base64_decode($base64string);

		// Create an image from the decoded data
		$image = @imagecreatefromstring($data);

		// Check if the image was created successfully
		if (!$image) {
			return false;
		}

		// Get the MIME type of the image
		$mime = image_type_to_mime_type(getimagesizefromstring($data)[2]);

		// Check if the MIME type is a valid image MIME type
		if (strpos($mime, 'image/') !== 0) {
			return false;
		}

		// If everything is okay, return true
		return true;
	}