CVE-2024-29272: Insufficient Information (NVD-CWE-noinfo)
value: Arbitrary File Upload vulnerability in VvvebJs before version 1.7.5, allows unauthenticated remote attackers to execute arbitrary code and obtain sensitive information via the sanitizeFileName parameter in save.php.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-22T04:15Z
Repository: https://github.com/givanz/VvvebJs
Language: PHP
Files changed: save.php, scan.php, upload.php
Commit date: 2024-03-02 02:15:52 +0200
Commit message: Fixed vulnerabilities reported by João Victor https://github.com/givanz/VvvebJs/issues/339

Diff: diff --git a/save.php b/save.php
index 0bce4f2..0a404b7 100644
--- a/save.php
+++ b/save.php
@@ -16,14 +16,33 @@ limitations under the License.
 
 https://github.com/givanz/VvvebJs
 */
 
 define('MAX_FILE_LIMIT', 1024 * 1024 * 2);//2 Megabytes max html file size
+define('ALLOW_PHP', false);//check if saved html contains php tag and don't save if not allowed
+define('ALLOWED_OEMBED_DOMAINS', [
+	'https://www.youtube.com/', 
+	'https://www.vimeo.com/', 
+	'https://www.twitter.com/'
+]);//load urls only from allowed websites for oembed
 
 function sanitizeFileName($file, $allowedExtension = 'html') {
+	$basename = basename($file);
+	$disallow = ['.htaccess', 'passwd'];
+	if (in_array($basename, $disallow)) {
+		showError('Filename not allowed!');
+		return '';
+	}
+	
 	//sanitize, remove double dot .. and remove get parameters if any
-	$file = __DIR__ . '/' . preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $file)));
+	$file = preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $file)));
+	
+	if ($file) {
+		$file = __DIR__ . DIRECTORY_SEPARATOR . $file;
+	} else {
+		return '';
+	}
 	
 	//allow only .html extension
 	if ($allowedExtension) {
 		$file = preg_replace('/\.[^.]+$/', '', $file) . ".$allowedExtension";
 	}
@@ -33,34 +52,53 @@ function sanitizeFileName($file, $allowedExtension = 'html') {
 function showError($error) {
 	header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
 	die($error);
 }
 
+function validOembedUrl($url) {
+	foreach (ALLOWED_OEMBED_DOMAINS as $domain) {
+		if (strpos($url, $domain) === 0) {
+			return true;
+		}
+	}
+	
+	return false;
+}
+
 $html   = '';
 $file   = '';
 $action = '';
 
 if (isset($_POST['startTemplateUrl']) && !empty($_POST['startTemplateUrl'])) {
 	$startTemplateUrl = sanitizeFileName($_POST['startTemplateUrl']);
-	$html = file_get_contents($startTemplateUrl);
+	$html = '';
+	if ($startTemplateUrl) {
+		$html = file_get_contents($startTemplateUrl);
+	}
 } else if (isset($_POST['html'])){
 	$html = substr($_POST['html'], 0, MAX_FILE_LIMIT);
+	if (!ALLOW_PHP) {
+		//if (strpos($html, '<?php') !== false) {
+		if (preg_match('@<\?php|<\? |<\?=|<\s*script\s*language\s*=\s*"\s*php\s*"\s*>@', $html)) {
+			showError('PHP not allowed!');
+		}
+	}
 }
 
 if (isset($_POST['file'])) {
-	$file = sanitizeFileName($_POST['file'], false);
+	$file = sanitizeFileName($_POST['file']);
 }
 
 if (isset($_GET['action'])) {
 	$action = $_GET['action'];
 }
 
 if ($action) {
 	//file manager actions, delete and rename
 	switch ($action) {
 		case 'rename':
-			$newfile = sanitizeFileName($_POST['newfile'], false);
+			$newfile = sanitizeFileName($_POST['newfile']);
 			if ($file && $newfile) {
 				if (rename($file, $newfile)) {
 					echo "File '$file' renamed to '$newfile'";
 				} else {
 					showError("Error renaming file '$file' renamed to '$newfile'");
@@ -83,32 +121,41 @@ if ($action) {
 			$html = $_POST['html'] ?? false;
 			
 			if ($type && $name && $html) {
 				
 				$file = sanitizeFileName("$type/$name");
-				$dir = dirname($file);
-				if (!is_dir($dir)) {
-					echo "$dir folder does not exist\n";
-					if (mkdir($dir, 0777, true)) {
-						echo "$dir folder was created\n";
+				if ($file) {
+					$dir = dirname($file);
+					if (!is_dir($dir)) {
+						echo "$dir folder does not exist\n";
+						if (mkdir($dir, 0777, true)) {
+							echo "$dir folder was created\n";
+						} else {
+							showError("Error creating folder '$dir'\n");
+						}				
+					}
+					
+					if (file_put_contents($file, $html)) {
+						echo "File saved '$file'";
 					} else {
-						showError("Error creating folder '$dir'\n");
-					}				
-				}
-				
-				if (file_put_contents($file, $html)) {
-					echo "File saved '$file'";
+						showError("Error saving file '$file'\nPossible causes are missing write permission or incorrect file path!");
+					}
 				} else {
-					showError("Error saving file '$file'\nPossible causes are missing write permission or incorrect file path!");
+					showError('Invalid filename!');
 				}
 			} else {
 				showError("Missing reusable element data!\n");
 			}
 		break;
 		case 'oembedProxy':
-			header('Content-Type: application/json');
-			echo file_get_contents($_GET['url']);
+			$url = $_GET['url'] ?? '';
+			if (validOembedUrl($url)) {
+				header('Content-Type: application/json');
+				echo file_get_contents($url);
+			} else {
+				showError('Invalid url!');
+			}
 		break;
 		default:
 			showError("Invalid action '$action'!");
 	}
 } else {
diff --git a/scan.php b/scan.php
index e49766b..cec79b1 100644
--- a/scan.php
+++ b/scan.php
@@ -17,17 +17,23 @@ limitations under the License.
 https://github.com/givanz/VvvebJs
 */
 
 //scan media folder for all files to display in media modal
 
-if (isset($_POST['mediaPath'])) {
-	define('UPLOAD_PATH', $_POST['mediaPath']);
+function sanitizePath($path) {
+	//sanitize, remove double dot .. and remove get parameters if any
+	$path = preg_replace('@/+@' , DIRECTORY_SEPARATOR, preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $path))));
+	return $path;
+}
+
+if (isset($_POST['mediaPath']) && ($path = sanitizePath(substr($_POST['mediaPath'], 0, 256)))) {
+	define('UPLOAD_PATH', $path);
 } else {
 	define('UPLOAD_PATH', 'media');
 }
 
-$scandir = __DIR__ . '/' . UPLOAD_PATH;
+$scandir = __DIR__ . DIRECTORY_SEPARATOR. UPLOAD_PATH;
 
 // Run the recursive function
 // This function scans the files folder recursively, and builds a large array
 
 $scan = function ($dir) use ($scandir, &$scan) {
diff --git a/upload.php b/upload.php
index 2bc288e..d4410cd 100644
--- a/upload.php
+++ b/upload.php
@@ -20,49 +20,54 @@ https://github.com/givanz/VvvebJs
 /*
 This script is used by image upload input to save the image on the server and return the image url to be set as image src attribute.
 */ 
 
 $uploadDenyExtensions  = ['php'];
-$uploadAllowExtensions = ['ico','jpg','jpeg','png','gif','webp'];
+$uploadAllowExtensions = ['ico','jpg','jpeg','png','gif','webp','svg'];
 
 function showError($error) {
 	header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
 	die($error);
 }
 
 function sanitizeFileName($file)
 {
+	$disallow = ['.htaccess', 'passwd'];
+	$file = str_replace($disallow, '', $file);
+	
 	//sanitize, remove double dot .. and remove get parameters if any
 	$file = preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $file)));
+	
 	return $file;
 }
 
-
-define('UPLOAD_FOLDER', __DIR__ . '/');
+define('UPLOAD_FOLDER', __DIR__ . DIRECTORY_SEPARATOR);
 if (isset($_POST['mediaPath'])) {
-	define('UPLOAD_PATH', sanitizeFileName($_POST['mediaPath']) .'/');
+	define('UPLOAD_PATH', sanitizeFileName($_POST['mediaPath']) . DIRECTORY_SEPARATOR);
 } else {
-	define('UPLOAD_PATH', '/');
+	define('UPLOAD_PATH', DIRECTORY_SEPARATOR);
+}
+
+$fileName  = sanitizeFileName($_FILES['file']['name']);
+if (!$fileName) {
+	showError('Invalid filename!');
 }
 
-$fileName  = $_FILES['file']['name'];
 $extension = strtolower(substr($fileName, strrpos($fileName, '.') + 1));
 
 //check if extension is on deny list
 if (in_array($extension, $uploadDenyExtensions)) {
 	showError("File type $extension not allowed!");
 }
 
-/*
 //comment deny code above and uncomment this code to change to a more restrictive allowed list
 // check if extension is on allow list
 if (!in_array($extension, $uploadAllowExtensions)) {
 	showError("File type $extension not allowed!");
 }
-*/
 
-$destination = UPLOAD_FOLDER . UPLOAD_PATH . '/' . $fileName;
+$destination = UPLOAD_FOLDER . UPLOAD_PATH . DIRECTORY_SEPARATOR . $fileName;
 move_uploaded_file($_FILES['file']['tmp_name'], $destination);
 
 if (isset($_POST['onlyFilename'])) {
 	echo $fileName;
 } else {

Vulnerable code: 	$file = __DIR__ . '/' . preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $file)));
	$html = file_get_contents($startTemplateUrl);
	$file = sanitizeFileName($_POST['file'], false);
			$newfile = sanitizeFileName($_POST['newfile'], false);
				$dir = dirname($file);
				if (!is_dir($dir)) {
					echo "$dir folder does not exist\n";
					if (mkdir($dir, 0777, true)) {
						echo "$dir folder was created\n";
						showError("Error creating folder '$dir'\n");
					}				
				}
				
				if (file_put_contents($file, $html)) {
					echo "File saved '$file'";
					showError("Error saving file '$file'\nPossible causes are missing write permission or incorrect file path!");
			header('Content-Type: application/json');
			echo file_get_contents($_GET['url']);
if (isset($_POST['mediaPath'])) {
	define('UPLOAD_PATH', $_POST['mediaPath']);
$scandir = __DIR__ . '/' . UPLOAD_PATH;
$uploadAllowExtensions = ['ico','jpg','jpeg','png','gif','webp'];

define('UPLOAD_FOLDER', __DIR__ . '/');
	define('UPLOAD_PATH', sanitizeFileName($_POST['mediaPath']) .'/');
	define('UPLOAD_PATH', '/');
$fileName  = $_FILES['file']['name'];
/*
*/
$destination = UPLOAD_FOLDER . UPLOAD_PATH . '/' . $fileName;
Fixed code: define('ALLOW_PHP', false);//check if saved html contains php tag and don't save if not allowed
define('ALLOWED_OEMBED_DOMAINS', [
	'https://www.youtube.com/', 
	'https://www.vimeo.com/', 
	'https://www.twitter.com/'
]);//load urls only from allowed websites for oembed
	$basename = basename($file);
	$disallow = ['.htaccess', 'passwd'];
	if (in_array($basename, $disallow)) {
		showError('Filename not allowed!');
		return '';
	}
	
	$file = preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $file)));
	
	if ($file) {
		$file = __DIR__ . DIRECTORY_SEPARATOR . $file;
	} else {
		return '';
	}
function validOembedUrl($url) {
	foreach (ALLOWED_OEMBED_DOMAINS as $domain) {
		if (strpos($url, $domain) === 0) {
			return true;
		}
	}
	
	return false;
}

	$html = '';
	if ($startTemplateUrl) {
		$html = file_get_contents($startTemplateUrl);
	}
	if (!ALLOW_PHP) {
		//if (strpos($html, '<?php') !== false) {
		if (preg_match('@<\?php|<\? |<\?=|<\s*script\s*language\s*=\s*"\s*php\s*"\s*>@', $html)) {
			showError('PHP not allowed!');
		}
	}
	$file = sanitizeFileName($_POST['file']);
			$newfile = sanitizeFileName($_POST['newfile']);
				if ($file) {
					$dir = dirname($file);
					if (!is_dir($dir)) {
						echo "$dir folder does not exist\n";
						if (mkdir($dir, 0777, true)) {
							echo "$dir folder was created\n";
						} else {
							showError("Error creating folder '$dir'\n");
						}				
					}
					
					if (file_put_contents($file, $html)) {
						echo "File saved '$file'";
						showError("Error saving file '$file'\nPossible causes are missing write permission or incorrect file path!");
					}
					showError('Invalid filename!');
			$url = $_GET['url'] ?? '';
			if (validOembedUrl($url)) {
				header('Content-Type: application/json');
				echo file_get_contents($url);
			} else {
				showError('Invalid url!');
			}
function sanitizePath($path) {
	//sanitize, remove double dot .. and remove get parameters if any
	$path = preg_replace('@/+@' , DIRECTORY_SEPARATOR, preg_replace('@\?.*$@' , '', preg_replace('@\.{2,}@' , '', preg_replace('@[^\/\\a-zA-Z0-9\-\._]@', '', $path))));
	return $path;
}

if (isset($_POST['mediaPath']) && ($path = sanitizePath(substr($_POST['mediaPath'], 0, 256)))) {
	define('UPLOAD_PATH', $path);
$scandir = __DIR__ . DIRECTORY_SEPARATOR. UPLOAD_PATH;
$uploadAllowExtensions = ['ico','jpg','jpeg','png','gif','webp','svg'];
	$disallow = ['.htaccess', 'passwd'];
	$file = str_replace($disallow, '', $file);
	
	
define('UPLOAD_FOLDER', __DIR__ . DIRECTORY_SEPARATOR);
	define('UPLOAD_PATH', sanitizeFileName($_POST['mediaPath']) . DIRECTORY_SEPARATOR);
	define('UPLOAD_PATH', DIRECTORY_SEPARATOR);
}

$fileName  = sanitizeFileName($_FILES['file']['name']);
if (!$fileName) {
	showError('Invalid filename!');
$destination = UPLOAD_FOLDER . UPLOAD_PATH . DIRECTORY_SEPARATOR . $fileName;