CVE-2021-44664: Unrestricted Upload of File with Dangerous Type (CWE-434)
value: An Authenticated Remote Code Exection (RCE) vulnerability exists in Xerte through 3.9 in website_code/php/import/fileupload.php by uploading a maliciously crafted PHP file though the project interface disguised as a language file to bypasses the upload filters. Attackers can manipulate the files destination by abusing path traversal in the 'mediapath' variable.
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: 2022-02-24T21:15Z
Repository: https://github.com/thexerteproject/xerteonlinetoolkits
Language: PHP
Files changed: editor/uploadImage.php, modules/decision/engine/upload.php, modules/site/engine/upload.php, website_code/php/import/fileupload.php
Commit date: 2021-03-22 21:58:29 +0100
Commit message: Security! Check and verify paths used by move_uploaded_file

 - Make sure uploaded files can only be saved in the USER-FILES
   subfolder

Diff: diff --git a/editor/uploadImage.php b/editor/uploadImage.php
index 4488f2915..2893e12e5 100644
--- a/editor/uploadImage.php
+++ b/editor/uploadImage.php
@@ -69,10 +69,21 @@ if (!isset($_FILES['upload']))
 
     echo json_encode($response);
     exit(-1);
 }
 
+// Check upload path, should contain USER-FILES
+if (strpos($_REQUEST['uploadURL'], 'USER-FILES') === false)
+{
+    // Invalid folder, reject!
+    $response->uploaded = 0;
+    $response->error = IMAGEUPOLOAD_NOT_UPLOADED;
+
+    echo json_encode($response);
+    exit(-1);
+}
+
 if (isset($_FILES['upload']['error']) && $_FILES['upload']['error'] != 0)
 {
     switch($_FILES['upload']['error']) {
         case UPLOAD_ERR_INI_SIZE:
             $mesg = IMAGEUPLOAD_TOO_LARGE;
diff --git a/modules/decision/engine/upload.php b/modules/decision/engine/upload.php
index cec4180c6..e8922bf5c 100644
--- a/modules/decision/engine/upload.php
+++ b/modules/decision/engine/upload.php
@@ -94,10 +94,12 @@ else {
 $pass = true;
 if (strpos($_FILES['Filedata']['name'], '../') !== false)
     $pass = false;
 if (strpos($_FILES['Filedata']['name'], '...') !== false)
     $pass = false;
+if (strpos($_GET['path'], 'USER-FILES') === false)
+    $pass = false;
 
 if ($pass === false) {
     receive_message($_SESSION['toolkits_logon_username'], "UPLOAD", "CRITICAL", "Invalid filename: " . $_FILES['Filedata']['name'], "Invalid filename: " . $_FILES['Filedata']['name']);
     exit();
 }
diff --git a/modules/site/engine/upload.php b/modules/site/engine/upload.php
index 4ba77c0ee..6f266e985 100644
--- a/modules/site/engine/upload.php
+++ b/modules/site/engine/upload.php
@@ -77,10 +77,12 @@ if (empty($_FILES)) {
 $pass = true;
 if (strpos($_FILES['Filedata']['name'], '../') !== false)
     $pass = false;
 if (strpos($_FILES['Filedata']['name'], '...') !== false)
     $pass = false;
+if (strpos($_GET['path'], 'USER-FILES') === false)
+    $pass = false;
 
 if ($pass === false) {
     receive_message($_SESSION['toolkits_logon_username'], "UPLOAD", "CRITICAL", "Invalid filename: " . $_FILES['Filedata']['name'], "Invalid filename: " . $_FILES['Filedata']['name']);
     exit();
 }
diff --git a/website_code/php/import/fileupload.php b/website_code/php/import/fileupload.php
index 43997854d..8d52d1650 100644
--- a/website_code/php/import/fileupload.php
+++ b/website_code/php/import/fileupload.php
@@ -53,10 +53,16 @@ function convertToBytes(string $from): ?int {
 if (!isset($_SESSION['toolkits_logon_username']))
 {
     _debug("Session is invalid or expired");
     die("Session is invalid or expired");
 }
+if (strpos($_POST['mediapath'], 'USER-FILES') === false)
+{
+    // Invalid upload path
+    _debug("Invalid or illegal mediapath");
+    die("Invalid or illegal mediapath");
+}
 
 _load_language_file("/website_code/php/import/fileupload.inc");
 
 if(apply_filters('editor_upload_file', $_FILES)){
 

Fixed code: // Check upload path, should contain USER-FILES
if (strpos($_REQUEST['uploadURL'], 'USER-FILES') === false)
{
    // Invalid folder, reject!
    $response->uploaded = 0;
    $response->error = IMAGEUPOLOAD_NOT_UPLOADED;

    echo json_encode($response);
    exit(-1);
}

if (strpos($_GET['path'], 'USER-FILES') === false)
    $pass = false;
if (strpos($_GET['path'], 'USER-FILES') === false)
    $pass = false;
if (strpos($_POST['mediapath'], 'USER-FILES') === false)
{
    // Invalid upload path
    _debug("Invalid or illegal mediapath");
    die("Invalid or illegal mediapath");
}