CVE-2018-11330: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: An issue was discovered in Pluck before 4.7.6. There is authenticated stored XSS because the character set for filenames is not properly restricted.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 4.8
Published: 2018-05-21T21:29Z
Repository: https://github.com/pluck-cms/pluck
Language: PHP
Files changed: data/inc/editpage.php, data/inc/files.php, data/inc/functions.all.php, data/inc/images.php, data/modules/.htaccess, data/themes/.htaccess, files/.htaccess, images/.htaccess
Commit date: 2018-05-21 12:21:01 +0200
Commit message: bugfix for XSS and backdoor file upload found by s7acktrac3 issue #58

Diff: diff --git a/data/inc/editpage.php b/data/inc/editpage.php
index 656b8d5..e5cb459 100755
--- a/data/inc/editpage.php
+++ b/data/inc/editpage.php
@@ -37,14 +37,14 @@ if (isset($_POST['save']) || isset($_POST['save_exit'])) {
 		}
 		else
 			$title = $_POST['title'];
 		//If we are editing an existing page, pass current seo-name.
 		if (isset($_GET['page'])) {
-			$seoname = save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data, $_GET['page']);
+			$seoname = latinOnlyInput(save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data, $_GET['page']));
 		} else {
 		//If we are creating a new page, don't pass seo-name.
-			$seoname = save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data);
+			$seoname = latinOnlyInput(save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data));
 		}
 		//If seoname is false, a file already exists with the same name
 		if (empty($seoname)) {
 			$error = show_error($lang['page']['name_exists'], 1, true);
 		}
diff --git a/data/inc/files.php b/data/inc/files.php
index 0f945ee..d7c9739 100755
--- a/data/inc/files.php
+++ b/data/inc/files.php
@@ -30,28 +30,31 @@ defined('IN_PLUCK') or exit('Access denied!');
 		<input type="submit" name="submit" value="<?php echo $lang['general']['upload']; ?>" />
 	</form>
 </div>
 <?php
 if (isset($_POST['submit'])) {
-		if (!copy($_FILES['filefile']['tmp_name'], 'files/'.$_FILES['filefile']['name']))
+		if (!copy($_FILES['filefile']['tmp_name'], 'files/'.latinOnlyInput(latinOnlyInput($_FILES['filefile']['name']))))
 			show_error($lang['general']['upload_failed'], 1);
 		else {
-			if (strcasecmp(substr($_FILES['filefile']['name'], -3),'php') == 0){
-				if (!rename('files/'.$_FILES['filefile']['name'], 'files/'.$_FILES['filefile']['name'].'.txt')){
+			$lastfour = strtolower(substr(latinOnlyInput($_FILES['filefile']['name']), -4));
+			$lastfive = strtolower(substr(latinOnlyInput($_FILES['filefile']['name']), -5));
+			$blockedExtentions = array('.php','php3','php4','php5','php6','php7','phtml');
+			if (in_array($lastfour, $blockedExtentions) or in_array($lastfive, $blockedExtentions) ){
+				if (!rename('files/'.latinOnlyInput($_FILES['filefile']['name']), 'files/'.latinOnlyInput($_FILES['filefile']['name']).'.txt')){
 					show_error($lang['general']['upload_failed'], 1);
 				}
-				chmod('files/'.$_FILES['filefile']['name'].'.txt', 0775);
+				chmod('files/'.latinOnlyInput($_FILES['filefile']['name']).'.txt', 0775);
 			}else{
-				chmod('files/'.$_FILES['filefile']['name'], 0775);
+				chmod('files/'.latinOnlyInput($_FILES['filefile']['name']), 0775);
 			}
 			?>
 				<div class="menudiv">
-					<strong><?php echo $lang['files']['name']; ?></strong> <?php echo $_FILES['filefile']['name']; ?>
+					<strong><?php echo $lang['files']['name']; ?></strong> <?php echo latinOnlyInput($_FILES['filefile']['name']); ?>
 					<br />
-					<strong><?php echo $lang['files']['size']; ?></strong> <?php echo $_FILES['filefile']['size'].' '.$lang['images']['bytes']; ?>
+					<strong><?php echo $lang['files']['size']; ?></strong> <?php echo latinOnlyInput($_FILES['filefile']['size']).' '.$lang['images']['bytes']; ?>
 					<br />
-					<strong><?php echo $lang['files']['type']; ?></strong> <?php echo $_FILES['filefile']['type']; ?>
+					<strong><?php echo $lang['files']['type']; ?></strong> <?php echo latinOnlyInput($_FILES['filefile']['type']); ?>
 					<br />
 					<strong><?php echo $lang['files']['success']; //TODO: Need to show this message another place, and with show_error(). ?></strong>
 				</div>
 			<?php
 		}
@@ -64,10 +67,11 @@ if (isset($_POST['submit'])) {
 //Show the uploaded images
 $files = read_dir_contents('files', 'files');
 	if ($files) {
 		natcasesort($files);
 		foreach ($files as $file) {
+			if (!($file == '.htaccess')){
 		?>
 			<div class="menudiv">
 				<span>
 					<img src="data/image/file.png" alt="" />
 				</span>
@@ -82,10 +86,11 @@ $files = read_dir_contents('files', 'files');
 						<img src="data/image/delete.png" title="<?php echo $lang['trashcan']['move_to_trash']; ?>" alt="<?php echo $lang['trashcan']['move_to_trash']; ?>" />
 					</a>
 				</span>
 			</div>
 			<?php
+			}
 		}
 		unset($files);
 	}
 	else
 		echo '<span class="kop4">'.$lang['general']['nothing_yet'].'</span>';
diff --git a/data/inc/functions.all.php b/data/inc/functions.all.php
index 31802e2..fbf9ee2 100644
--- a/data/inc/functions.all.php
+++ b/data/inc/functions.all.php
@@ -213,14 +213,32 @@ function sanitize($var, $html = true) {
  * @param string $var The url to sanitize.
  * @return string The sanitized variable.
  */
 function preventXSS($var) {
 	$var = str_replace('\'', '', $var);
+	$var = htmlspecialchars($var, ENT_COMPAT, 'UTF-8', false);
 
 	return $var;
 }
 
+/**
+ * latinOnlyInput from a URL, to make it ready for saving in a file.
+ *
+ * @since 4.7.5
+ * @package all
+ * @param string $var Variable 
+ * @return string The sanitized variable.
+ * 
+ * Added as bugfix for XSS and backdoor file upload found by s7acktrac3
+ */
+function latinOnlyInput($var) {
+	$var = str_replace(chr(0), '', $var);
+	$var = preg_replace("/[^a-zA-Z0-9.\ -_]+/", "", $var);;
+	return $var;
+}
+
+
 /**
  * Displays or returns an error, notice or success message.
  *
  * @since 4.7
  * @package all
diff --git a/data/inc/images.php b/data/inc/images.php
index 6f09d06..c006759 100644
--- a/data/inc/images.php
+++ b/data/inc/images.php
@@ -37,21 +37,21 @@ if (isset($_POST['submit'])) {
 		/* fix issue 44. Thanks to Klaus.  */
         $imagewhitelist = array('jfif', '.png', '.jpg', '.gif', 'jpeg');  
         if (!in_array(strtolower(substr($_FILES['imagefile']['name'], -4)), $imagewhitelist))
 			show_error($lang['general']['upload_failed'], 1);
 		/* end of fix issue 44. Thanks to Klaus.  */
-		if (!copy($_FILES['imagefile']['tmp_name'], 'images/'.$_FILES['imagefile']['name']))
+		if (!copy($_FILES['imagefile']['tmp_name'], 'images/'.latinOnlyInput($_FILES['imagefile']['name'])))
 			show_error($lang['general']['upload_failed'], 1);
 		else {
 			chmod('images/'.$_FILES['imagefile']['name'], 0666);
 			?>
 				<div class="menudiv">
-					<strong><?php echo $lang['images']['name']; ?></strong> <?php echo $_FILES['imagefile']['name']; ?>
+					<strong><?php echo $lang['images']['name']; ?></strong> <?php echo latinOnlyInput($_FILES['imagefile']['name']); ?>
 					<br />
-					<strong><?php echo $lang['images']['size']; ?></strong> <?php echo $_FILES['imagefile']['size'].' '.$lang['images']['bytes']; ?>
+					<strong><?php echo $lang['images']['size']; ?></strong> <?php echo latinOnlyInput($_FILES['imagefile']['size']).' '.$lang['images']['bytes']; ?>
 					<br />
-					<strong><?php echo $lang['images']['type']; ?></strong> <?php echo $_FILES['imagefile']['type']; ?>
+					<strong><?php echo $lang['images']['type']; ?></strong> <?php echo latinOnlyInput($_FILES['imagefile']['type']); ?>
 					<br />
 					<strong><?php echo $lang['images']['success']; //TODO: Need to show this message another place, and with show_error(). ?></strong>
 				</div>
 			<?php
 		}
@@ -65,10 +65,11 @@ if (isset($_POST['submit'])) {
 //Show the uploaded images
 $images = read_dir_contents('images', 'files');
 	if ($images) {
 		natcasesort($images);
 		foreach ($images as $image) {
+			if (!($image == '.htaccess')){
 		?>
 			<div class="menudiv">
 				<span>
 					<img src="data/image/image.png" alt="" />
 				</span>
@@ -83,10 +84,11 @@ $images = read_dir_contents('images', 'files');
 						<img src="data/image/delete.png" title="<?php echo $lang['trashcan']['move_to_trash']; ?>" alt="<?php echo $lang['trashcan']['move_to_trash']; ?>" />
 					</a>
 				</span>
 			</div>
 			<?php
+			}
 		}
 		unset($images);
 	}
 	else
 		echo '<span class="kop4">'.$lang['general']['nothing_yet'].'</span>';
diff --git a/data/modules/.htaccess b/data/modules/.htaccess
new file mode 100644
index 0000000..76635f2
--- /dev/null
+++ b/data/modules/.htaccess
@@ -0,0 +1,9 @@
+<FilesMatch \.php$>
+    SetHandler None
+</FilesMatch>
+
+<FilesMatch \.phtml>
+    SetHandler None
+</FilesMatch>
+
+Options -ExecCGI
diff --git a/data/themes/.htaccess b/data/themes/.htaccess
new file mode 100644
index 0000000..76635f2
--- /dev/null
+++ b/data/themes/.htaccess
@@ -0,0 +1,9 @@
+<FilesMatch \.php$>
+    SetHandler None
+</FilesMatch>
+
+<FilesMatch \.phtml>
+    SetHandler None
+</FilesMatch>
+
+Options -ExecCGI
diff --git a/files/.files b/files/.files
deleted file mode 100644
index e69de29..0000000
diff --git a/files/.htaccess b/files/.htaccess
new file mode 100644
index 0000000..76635f2
--- /dev/null
+++ b/files/.htaccess
@@ -0,0 +1,9 @@
+<FilesMatch \.php$>
+    SetHandler None
+</FilesMatch>
+
+<FilesMatch \.phtml>
+    SetHandler None
+</FilesMatch>
+
+Options -ExecCGI
diff --git a/images/.htaccess b/images/.htaccess
new file mode 100644
index 0000000..76635f2
--- /dev/null
+++ b/images/.htaccess
@@ -0,0 +1,9 @@
+<FilesMatch \.php$>
+    SetHandler None
+</FilesMatch>
+
+<FilesMatch \.phtml>
+    SetHandler None
+</FilesMatch>
+
+Options -ExecCGI
diff --git a/images/.images b/images/.images
deleted file mode 100644
index e69de29..0000000

Vulnerable code: 			$seoname = save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data, $_GET['page']);
			$seoname = save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data);
		if (!copy($_FILES['filefile']['tmp_name'], 'files/'.$_FILES['filefile']['name']))
			if (strcasecmp(substr($_FILES['filefile']['name'], -3),'php') == 0){
				if (!rename('files/'.$_FILES['filefile']['name'], 'files/'.$_FILES['filefile']['name'].'.txt')){
				chmod('files/'.$_FILES['filefile']['name'].'.txt', 0775);
				chmod('files/'.$_FILES['filefile']['name'], 0775);
					<strong><?php echo $lang['files']['name']; ?></strong> <?php echo $_FILES['filefile']['name']; ?>
					<strong><?php echo $lang['files']['size']; ?></strong> <?php echo $_FILES['filefile']['size'].' '.$lang['images']['bytes']; ?>
					<strong><?php echo $lang['files']['type']; ?></strong> <?php echo $_FILES['filefile']['type']; ?>
		if (!copy($_FILES['imagefile']['tmp_name'], 'images/'.$_FILES['imagefile']['name']))
					<strong><?php echo $lang['images']['name']; ?></strong> <?php echo $_FILES['imagefile']['name']; ?>
					<strong><?php echo $lang['images']['size']; ?></strong> <?php echo $_FILES['imagefile']['size'].' '.$lang['images']['bytes']; ?>
					<strong><?php echo $lang['images']['type']; ?></strong> <?php echo $_FILES['imagefile']['type']; ?>
Fixed code: 			$seoname = latinOnlyInput(save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data, $_GET['page']));
			$seoname = latinOnlyInput(save_page($title, $_POST['content'], $_POST['hidden'], $_POST['sub_page'], $_POST['description'], $_POST['keywords'], $module_additional_data));
		if (!copy($_FILES['filefile']['tmp_name'], 'files/'.latinOnlyInput(latinOnlyInput($_FILES['filefile']['name']))))
			$lastfour = strtolower(substr(latinOnlyInput($_FILES['filefile']['name']), -4));
			$lastfive = strtolower(substr(latinOnlyInput($_FILES['filefile']['name']), -5));
			$blockedExtentions = array('.php','php3','php4','php5','php6','php7','phtml');
			if (in_array($lastfour, $blockedExtentions) or in_array($lastfive, $blockedExtentions) ){
				if (!rename('files/'.latinOnlyInput($_FILES['filefile']['name']), 'files/'.latinOnlyInput($_FILES['filefile']['name']).'.txt')){
				chmod('files/'.latinOnlyInput($_FILES['filefile']['name']).'.txt', 0775);
				chmod('files/'.latinOnlyInput($_FILES['filefile']['name']), 0775);
					<strong><?php echo $lang['files']['name']; ?></strong> <?php echo latinOnlyInput($_FILES['filefile']['name']); ?>
					<strong><?php echo $lang['files']['size']; ?></strong> <?php echo latinOnlyInput($_FILES['filefile']['size']).' '.$lang['images']['bytes']; ?>
					<strong><?php echo $lang['files']['type']; ?></strong> <?php echo latinOnlyInput($_FILES['filefile']['type']); ?>
			if (!($file == '.htaccess')){
			}
	$var = htmlspecialchars($var, ENT_COMPAT, 'UTF-8', false);
/**
 * latinOnlyInput from a URL, to make it ready for saving in a file.
 *
 * @since 4.7.5
 * @package all
 * @param string $var Variable 
 * @return string The sanitized variable.
 * 
 * Added as bugfix for XSS and backdoor file upload found by s7acktrac3
 */
function latinOnlyInput($var) {
	$var = str_replace(chr(0), '', $var);
	$var = preg_replace("/[^a-zA-Z0-9.\ -_]+/", "", $var);;
	return $var;
}


		if (!copy($_FILES['imagefile']['tmp_name'], 'images/'.latinOnlyInput($_FILES['imagefile']['name'])))
					<strong><?php echo $lang['images']['name']; ?></strong> <?php echo latinOnlyInput($_FILES['imagefile']['name']); ?>
					<strong><?php echo $lang['images']['size']; ?></strong> <?php echo latinOnlyInput($_FILES['imagefile']['size']).' '.$lang['images']['bytes']; ?>
					<strong><?php echo $lang['images']['type']; ?></strong> <?php echo latinOnlyInput($_FILES['imagefile']['type']); ?>
			if (!($image == '.htaccess')){
			}
<FilesMatch \.php$>
    SetHandler None
</FilesMatch>

<FilesMatch \.phtml>
    SetHandler None
</FilesMatch>

Options -ExecCGI
<FilesMatch \.php$>
    SetHandler None
</FilesMatch>

<FilesMatch \.phtml>
    SetHandler None
</FilesMatch>

Options -ExecCGI
<FilesMatch \.php$>
    SetHandler None
</FilesMatch>

<FilesMatch \.phtml>
    SetHandler None
</FilesMatch>

Options -ExecCGI
<FilesMatch \.php$>
    SetHandler None
</FilesMatch>

<FilesMatch \.phtml>
    SetHandler None
</FilesMatch>

Options -ExecCGI