CVE-2018-1000658: Unrestricted Upload of File with Dangerous Type (CWE-434)
value: LimeSurvey version prior to 3.14.4 contains a file upload vulnerability in upload functionality that can result in an attacker gaining code execution via webshell. This attack appear to be exploitable via an authenticated user uploading a zip archive which can contains malicious php files that can be called under certain circumstances. This vulnerability appears to have been fixed in after commit 91d143230eb357260a19c8424b3005deb49a47f7 / version 3.14.4.
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: 2018-09-06T17:29Z
Repository: https://github.com/LimeSurvey/LimeSurvey
Language: PHP
Files changed: application/controllers/admin/export.php, application/controllers/admin/labels.php, application/controllers/admin/surveyadmin.php, application/core/Survey_Common_Action.php, application/helpers/common_helper.php
Commit date: 2018-08-16 16:59:51 +0200
Commit message: [security] Fixed issue #13960: vulnerabilities in file upload functionality


Diff: diff --git a/application/controllers/admin/export.php b/application/controllers/admin/export.php
index cc09bfde377..5f6f68a546d 100644
--- a/application/controllers/admin/export.php
+++ b/application/controllers/admin/export.php
@@ -346,11 +346,11 @@ class export extends Survey_Common_Action
         $filterstate = incompleteAnsFilterState();
         if (!Yii::app()->session['spssversion']) {
 // Default to 2 (16 and up)
             Yii::app()->session['spssversion'] = 2;
         }
-        $spssver = Yii::app()->request->getParam('spssver', Yii::app()->session['spssversion']); 
+        $spssver = Yii::app()->request->getParam('spssver', Yii::app()->session['spssversion']);
         Yii::app()->session['spssversion'] = $spssver;
 
         $length_varlabel = '231'; // Set the max text length of Variable Labels
         $length_vallabel = '120'; // Set the max text length of Value Labels
 
@@ -1148,11 +1148,11 @@ class export extends Survey_Common_Action
                 $quexmlpdf->$method(Yii::app()->request->getPost($s));
             }
 
 
             $lang = Yii::app()->request->getPost('save_language');
-            $tempdir = Yii::app()->getConfig("tempdir");
+
 
             // Setting the selected language for printout
             App()->setLanguage($lang);
 
             $quexmlpdf->setLanguage($lang);
@@ -1167,11 +1167,13 @@ class export extends Survey_Common_Action
             $quexmlpdf->create($quexmlpdf->createqueXML($quexml));
 
             //NEED TO GET QID from $quexmlpdf
             $qid = intval($quexmlpdf->getQuestionnaireId());
 
-            $zipdir = $this->_tempdir($tempdir);
+            Yii::import('application.helpers.common_helper', true);
+            $zipdir = createRandomTempDir();
+
 
             $f1 = "$zipdir/quexf_banding_{$qid}_{$lang}.xml";
             $f2 = "$zipdir/quexmlpdf_{$qid}_{$lang}.pdf";
             $f3 = "$zipdir/quexml_{$qid}_{$lang}.xml";
             $f4 = "$zipdir/readme.txt";
@@ -1183,11 +1185,11 @@ class export extends Survey_Common_Action
             file_put_contents($f3, $quexml);
             file_put_contents($f4, gT('This archive contains a PDF file of the survey, the queXML file of the survey and a queXF banding XML file which can be used with queXF: http://quexf.sourceforge.net/ for processing scanned surveys.'));
 
 
             Yii::app()->loadLibrary('admin.pclzip');
-            $zipfile = "$tempdir/quexmlpdf_{$qid}_{$lang}.zip";
+            $zipfile = Yii::app()->getConfig("tempdir").DIRECTORY_SEPARATOR."quexmlpdf_{$qid}_{$lang}.zip";
             $z = new PclZip($zipfile);
             $z->create($zipdir, PCLZIP_OPT_REMOVE_PATH, $zipdir);
 
             unlink($f1);
             unlink($f2);
@@ -1220,14 +1222,16 @@ class export extends Survey_Common_Action
         $oSurvey = Survey::model()->findByPk($iSurveyID);
         $assetsDir = substr(Template::getTemplateURL($oSurvey->template), 1);
         $fullAssetsDir = Template::getTemplatePath($oSurvey->template);
         $aLanguages = $oSurvey->getAllLanguages();
 
-        $tempdir = Yii::app()->getConfig("tempdir");
-        $zipdir = $this->_tempdir($tempdir);
+        Yii::import('application.helpers.common_helper', true);
+        $zipdir = createRandomTempDir();
 
         $fn = "printable_survey_".preg_replace('([^\w\s\d\-_~,;\[\]\(\).])','',$oSurvey->currentLanguageSettings->surveyls_title)."_{$oSurvey->primaryKey}.zip";
+
+        $tempdir = Yii::app()->getConfig("tempdir");
         $zipfile = "$tempdir/".$fn;
 
         Yii::app()->loadLibrary('admin.pclzip');
         $z = new PclZip($zipfile);
         $z->create($zipdir, PCLZIP_OPT_REMOVE_PATH, $zipdir);
diff --git a/application/controllers/admin/labels.php b/application/controllers/admin/labels.php
index 9a5542c708c..2cab63a2a8d 100644
--- a/application/controllers/admin/labels.php
+++ b/application/controllers/admin/labels.php
@@ -58,11 +58,12 @@ class labels extends Survey_Common_Action
             }
 
             // Create temporary directory
             // If dangerous content is unzipped
             // then no one will know the path
-            $extractdir = $this->_tempdir(Yii::app()->getConfig('tempdir'));
+            Yii::import('application.helpers.common_helper', true);
+            $extractdir = createRandomTempDir();
             $zipfilename = $_FILES['the_file']['tmp_name'];
             $basedestdir = Yii::app()->getConfig('uploaddir')."/labels";
             $destdir = $basedestdir."/$lid/";
 
             Yii::app()->loadLibrary('admin.pclzip');
diff --git a/application/controllers/admin/surveyadmin.php b/application/controllers/admin/surveyadmin.php
index 3229b9c7577..9bb13729557 100644
--- a/application/controllers/admin/surveyadmin.php
+++ b/application/controllers/admin/surveyadmin.php
@@ -270,11 +270,12 @@ class SurveyAdmin extends Survey_Common_Action
             }
 
             // Create temporary directory
             // If dangerous content is unzipped
             // then no one will know the path
-            $extractdir  = $this->_tempdir(Yii::app()->getConfig('tempdir'));
+            Yii::import('application.helpers.common_helper', true);
+            $extractdir  = createRandomTempDir();
             $zipfilename = $_FILES['the_file']['tmp_name'];
             $basedestdir = Yii::app()->getConfig('uploaddir')."/surveys";
             $destdir     = $basedestdir."/$iSurveyID/";
 
             Yii::app()->loadLibrary('admin.pclzip');
diff --git a/application/core/Survey_Common_Action.php b/application/core/Survey_Common_Action.php
index 6efa3bddb34..66e505e8239 100644
--- a/application/core/Survey_Common_Action.php
+++ b/application/core/Survey_Common_Action.php
@@ -1232,32 +1232,10 @@ class Survey_Common_Action extends CAction
         }
 
         return array($aImportedFilesInfo, $aErrorFilesInfo);
     }
 
-    /**
-     * Creates a temporary directory
-     *
-     * @access protected
-     * @param string $dir
-     * @param string $prefix
-     * @param int $mode
-     * @return string
-     */
-    protected function _tempdir($dir, $prefix = '', $mode = 0700)
-    {
-        if (substr($dir, -1) != DIRECTORY_SEPARATOR) {
-            $dir .= DIRECTORY_SEPARATOR;
-        }
-
-        do {
-            $path = $dir.$prefix.mt_rand(0, 9999999);
-        } while (!mkdir($path, $mode));
-
-        return $path;
-    }
-
     /**
      * Get extra menus from plugins that are using event beforeAdminMenuRender
      *
      * @param array $aData
      * @return array<ExtraMenu>
diff --git a/application/helpers/common_helper.php b/application/helpers/common_helper.php
index c7849ef4710..0ebd93ffd1f 100644
--- a/application/helpers/common_helper.php
+++ b/application/helpers/common_helper.php
@@ -4959,10 +4959,78 @@ function isAssociativeArray($array){
         }
     }
     return false;
 }
 
+
+/**
+* Create a directory in tmp dir using a random string
+*
+* @param  string $dir      the temp directory (if empty will use the one from configuration)
+* @param  string $prefix   wanted prefix for the directory
+* @param  int    $mode     wanted  file mode for this directory
+* @return string           the path of the created directory
+*/
+function createRandomTempDir($dir=null, $prefix = '', $mode = 0700)
+{
+
+    $sDir = (empty($dir)) ? Yii::app()->getConfig('tempdir') : get_absolute_path ($dir);
+
+    if (substr($sDir, -1) != DIRECTORY_SEPARATOR) {
+        $sDir .= DIRECTORY_SEPARATOR;
+    }
+
+    do {
+        $sRandomString = getRandomString();
+        $path = $sDir.$prefix.$sRandomString;
+    }
+    while (!mkdir($path, $mode));
+
+    return $path;
+}
+
+/**
+ * Generate a random string, using openssl if available, else using md5
+ * @param  int    $length wanted lenght of the random string (only for openssl mode)
+ * @return string
+ */
+function getRandomString($length=32){
+
+    if ( function_exists('openssl_random_pseudo_bytes') ) {
+        $token = "";
+        $codeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+        $codeAlphabet.= "abcdefghijklmnopqrstuvwxyz";
+        $codeAlphabet.= "0123456789";
+        for($i=0;$i<$length;$i++){
+            $token .= $codeAlphabet[crypto_rand_secure(0,strlen($codeAlphabet))];
+        }
+    }else{
+        $token = md5(uniqid(rand(), true));
+    }
+    return $token;
+}
+
+/**
+ * Get a random number between two values using openssl_random_pseudo_bytes
+ * @param  int    $min
+ * @param  int    $max
+ * @return string
+ */
+function crypto_rand_secure($min, $max) {
+        $range = $max - $min;
+        if ($range < 0) return $min; // not so random...
+        $log = log($range, 2);
+        $bytes = (int) ($log / 8) + 1; // length in bytes
+        $bits = (int) $log + 1; // length in bits
+        $filter = (int) (1 << $bits) - 1; // set all lower bits to 1
+        do {
+            $rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));
+            $rnd = $rnd & $filter; // discard irrelevant bits
+        } while ($rnd >= $range);
+        return $min + $rnd;
+}
+
 /**
  * Test if a given zip file is Zip Bomb
  * see comment here : http://php.net/manual/en/function.zip-entry-filesize.php
  * @param string $zip_filename
  * @return int

Vulnerable code:         $spssver = Yii::app()->request->getParam('spssver', Yii::app()->session['spssversion']); 
            $tempdir = Yii::app()->getConfig("tempdir");
            $zipdir = $this->_tempdir($tempdir);
            $zipfile = "$tempdir/quexmlpdf_{$qid}_{$lang}.zip";
        $tempdir = Yii::app()->getConfig("tempdir");
        $zipdir = $this->_tempdir($tempdir);
            $extractdir = $this->_tempdir(Yii::app()->getConfig('tempdir'));
            $extractdir  = $this->_tempdir(Yii::app()->getConfig('tempdir'));
    /**
     * Creates a temporary directory
     *
     * @access protected
     * @param string $dir
     * @param string $prefix
     * @param int $mode
     * @return string
     */
    protected function _tempdir($dir, $prefix = '', $mode = 0700)
    {
        if (substr($dir, -1) != DIRECTORY_SEPARATOR) {
            $dir .= DIRECTORY_SEPARATOR;
        }

        do {
            $path = $dir.$prefix.mt_rand(0, 9999999);
        } while (!mkdir($path, $mode));

        return $path;
    }

Fixed code:         $spssver = Yii::app()->request->getParam('spssver', Yii::app()->session['spssversion']);

            Yii::import('application.helpers.common_helper', true);
            $zipdir = createRandomTempDir();

            $zipfile = Yii::app()->getConfig("tempdir").DIRECTORY_SEPARATOR."quexmlpdf_{$qid}_{$lang}.zip";
        Yii::import('application.helpers.common_helper', true);
        $zipdir = createRandomTempDir();

        $tempdir = Yii::app()->getConfig("tempdir");
            Yii::import('application.helpers.common_helper', true);
            $extractdir = createRandomTempDir();
            Yii::import('application.helpers.common_helper', true);
            $extractdir  = createRandomTempDir();

/**
* Create a directory in tmp dir using a random string
*
* @param  string $dir      the temp directory (if empty will use the one from configuration)
* @param  string $prefix   wanted prefix for the directory
* @param  int    $mode     wanted  file mode for this directory
* @return string           the path of the created directory
*/
function createRandomTempDir($dir=null, $prefix = '', $mode = 0700)
{

    $sDir = (empty($dir)) ? Yii::app()->getConfig('tempdir') : get_absolute_path ($dir);

    if (substr($sDir, -1) != DIRECTORY_SEPARATOR) {
        $sDir .= DIRECTORY_SEPARATOR;
    }

    do {
        $sRandomString = getRandomString();
        $path = $sDir.$prefix.$sRandomString;
    }
    while (!mkdir($path, $mode));

    return $path;
}

/**
 * Generate a random string, using openssl if available, else using md5
 * @param  int    $length wanted lenght of the random string (only for openssl mode)
 * @return string
 */
function getRandomString($length=32){

    if ( function_exists('openssl_random_pseudo_bytes') ) {
        $token = "";
        $codeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        $codeAlphabet.= "abcdefghijklmnopqrstuvwxyz";
        $codeAlphabet.= "0123456789";
        for($i=0;$i<$length;$i++){
            $token .= $codeAlphabet[crypto_rand_secure(0,strlen($codeAlphabet))];
        }
    }else{
        $token = md5(uniqid(rand(), true));
    }
    return $token;
}

/**
 * Get a random number between two values using openssl_random_pseudo_bytes
 * @param  int    $min
 * @param  int    $max
 * @return string
 */
function crypto_rand_secure($min, $max) {
        $range = $max - $min;
        if ($range < 0) return $min; // not so random...
        $log = log($range, 2);
        $bytes = (int) ($log / 8) + 1; // length in bytes
        $bits = (int) $log + 1; // length in bits
        $filter = (int) (1 << $bits) - 1; // set all lower bits to 1
        do {
            $rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));
            $rnd = $rnd & $filter; // discard irrelevant bits
        } while ($rnd >= $range);
        return $min + $rnd;
}