CVE-2023-4652 (PHP, published 2023-08-31T01:15Z)
Description: value: Cross-site Scripting (XSS) - Stored in GitHub repository instantsoft/icms2 prior to 2.16.1-git.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
Repository: https://github.com/instantsoft/icms2
Files changed:
system/controllers/admin/actions/install.php
system/core/uploader.php
system/core/uploadfile.php
Commit message: Fixed upload XSS with wrong extension

Vulnerable code:
        $result = $this->cms_uploader->upload($this->upload_name, $this->upload_exts, 0, $this->installer_upload_path);

    private $site_cfg = array();
    private $last_error = false;
    private $upload_errors = array();
    public function __construct() {
        $this->upload_errors = array(
        );
        $this->user_id = cmsUser::getInstance()->id;
        $mime_types = (new cmsConfigs('mimetypes.php'))->getAll();

            if(isset($mime_types[$mime])){
                $this->allowed_mime_ext[] = $mime_types[$mime];
        $this->file_name = mb_substr(trim($name), 0, 64); return $this;
        $this->user_id = $id; return $this;
//============================================================================//
    public function getMaxUploadSize(){

    public function isUploaded($name){
        if (!isset($_FILES[$name])) { return false; }
            if(isset($_FILES[$name]['error'])){
                if(isset($this->upload_errors[$_FILES[$name]['error']]) && $this->upload_errors[$_FILES[$name]['error']] !== UPLOAD_ERR_OK){


    public function isUploadedXHR($name){
    public function isUploadedFromLink($name){
        $this->allow_remote = true; return $this;
    }
    public function disableRemoteUpload() {
        $this->allow_remote = false; return $this;
    }

    private function getFileName($path, $file_ext, $file_name = false) {

        if(!$file_name){
            if($this->file_name){
                $file_name = str_replace('.'.$file_ext, '', files_sanitize_name($this->file_name.'.'.$file_ext));
            } else {
                $file_name = substr(md5(microtime(true)), 0, 8);
            }
        }

        if (file_exists($path.$file_name.'.'.$file_ext)) {
            return $this->getFileName($path, $file_ext, $file_name.'_'.md5(microtime(true)));
        }
        return $file_name.'.'.$file_ext;
//============================================================================//
//============================================================================//

     * Этот метод устаревший, используйте класс cmsImages
    public function resizeImage($source_file, $size){

        $dest_dir  = $this->getUploadDestinationDirectory();
        $dest_ext  = pathinfo($source_file, PATHINFO_EXTENSION);
        $dest_name = $this->getFileName($dest_dir, $dest_ext);

        $dest_file = $dest_dir . $dest_name;

        if (!isset($size['height'])) { $size['height'] = 0; }
        if (!isset($size['quality'])) { $size['quality'] = 90; }

        if (img_resize($source_file, $dest_file, $size['width'], $size['height'], $size['is_square'], $size['quality'])) {

            return str_replace($this->site_cfg->upload_path, '', $dest_file);

        }
        return false;
    private function checkExt($ext, $allowed_ext) {

        if($allowed_ext === false){
            return true;
        }

        if(empty($ext)){ return false; }

        if(!is_array($allowed_ext)){
            $allowed_ext = explode(',', (string)$allowed_ext);
        }

        $allowed = array();
        foreach($allowed_ext as $aext){
            $aext = mb_strtolower(trim(trim((string)$aext, '., ')));
            if(empty($aext)){
                continue;
            $allowed[] = $aext;
        return in_array(mb_strtolower($ext), $allowed, true);

    }

    private function isMimeTypeAllowed($file_path) {

        $finfo = finfo_open(FILEINFO_MIME_TYPE);

        $file_mime = finfo_file($finfo, $file_path);

        if($file_mime === false){ return false; }

        return in_array($file_mime, $this->allowed_mime);
//============================================================================//
//============================================================================//

     * @param string $post_filename Название поля с файлом в массиве $_FILES
     * @param string $destination Папка назначения (внутри пути upload)
    public function upload($post_filename, $allowed_ext = false, $allowed_size = 0, $destination = false){
        if ($this->isUploadedFromLink($post_filename)){
            return $this->uploadFromLink($post_filename, $allowed_ext, $allowed_size, $destination);
        if ($this->isUploadedXHR($post_filename)){
            return $this->uploadXHR($post_filename, $allowed_ext, $allowed_size, $destination);
        if ($this->isUploaded($post_filename)){
            return $this->uploadForm($post_filename, $allowed_ext, $allowed_size, $destination);
        return array(
        );

     * @param string $post_filename Название поля с файлом в массиве $_FILES
     * @param string $allowed_ext Список допустимых расширений (через запятую)
     * @param string $allowed_size Максимальный размер файла (в байтах)
     * @param string $destination Папка назначения (внутри пути upload)
    public function uploadForm($post_filename, $allowed_ext = false, $allowed_size = 0, $destination = false){
        $source     = $_FILES[$post_filename]['tmp_name'];
        $error_code = $_FILES[$post_filename]['error'];
        $dest_size  = (int)$_FILES[$post_filename]['size'];
        $dest_name  = files_sanitize_name($_FILES[$post_filename]['name']);
        $dest_ext   = pathinfo($dest_name, PATHINFO_EXTENSION);
        if(!$this->checkExt($dest_ext, $allowed_ext)){
            return array(
                'error'   => LANG_UPLOAD_ERR_MIME,
            );
        }

        if($this->allowed_mime !== false){
            if(!$this->isMimeTypeAllowed($source)){
                return array(
                    'error'   => LANG_UPLOAD_ERR_MIME.'. '.sprintf(LANG_PARSER_FILE_EXTS_FIELD_HINT, implode(', ', $this->allowed_mime_ext)),
                    'success' => false,
                    'name'    => $dest_name
                );
            }
        if ($allowed_size){
            if ($dest_size > $allowed_size){
                return array(
                );
        if (!$destination){

//============================================================================//
//============================================================================//

    public function uploadFromLink($post_filename, $allowed_ext = false, $allowed_size = 0, $destination = false) {
        if (function_exists('curl_init')){
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_URL, $link);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_NOBODY, true);
            curl_setopt($curl, CURLOPT_TIMEOUT, 5);
            $headers = curl_exec($curl);
            curl_close($curl);
            $matches = array();
            if(preg_match("/(?:Location:|URI:)([^\n]+)*/is", $headers, $matches)){
                $url = trim($matches[1]);
                if(strpos($url, 'http') !== 0){
                    $url_data = parse_url($link);
                    $link = $url_data['scheme'].'://'.$url_data['host'].$url;
                } else {
                    $link = $url;
                }
                $_POST[$post_filename] = $link;
                return $this->uploadFromLink($post_filename, $allowed_ext, $allowed_size, $destination);
            }
            if(preg_match('#filename="([^"]+)#uis', $headers, $matches)){
                $file_name = trim($matches[1]);
        }
        $dest_ext  = strtolower(pathinfo(parse_url($file_name, PHP_URL_PATH), PATHINFO_EXTENSION));
        $dest_name = files_sanitize_name($file_name);
        if(!$this->checkExt($dest_ext, $allowed_ext)){
            return array(
                'error'   => LANG_UPLOAD_ERR_MIME,
                'success' => false,
                'name'    => $dest_name
            );
        if(!$file_bin){
            return array(
                'error'   => LANG_UPLOAD_ERR_PARTIAL,
            );
        $image_size = strlen($file_bin);

        if ($allowed_size){
            if ($image_size > $allowed_size){
                return array(
                    'error'   => sprintf(LANG_UPLOAD_ERR_INI_SIZE, files_format_bytes($allowed_size)),
                    'success' => false,
                    'name'    => $dest_name
                );
            }
        }

        if (!$destination){
            $destination = $this->getUploadDestinationDirectory();
        } else {
            $destination = $this->site_cfg->upload_path . $destination.'/';
        }
        $destination .= $this->getFileName($destination, $dest_ext);
		$f = fopen($destination, 'w+');
		fwrite($f, $file_bin);
        fclose($f);
        if($this->allowed_mime !== false){
            if(!$this->isMimeTypeAllowed($destination)){
                @unlink($destination);
                return array(
                    'error'   => LANG_UPLOAD_ERR_MIME.'. '.sprintf(LANG_PARSER_FILE_EXTS_FIELD_HINT, implode(', ', $this->allowed_mime_ext)),
                    'success' => false,
                    'name'    => $dest_name
                );
            }
        return array(
            'success' => true,
            'path'    => $destination,
            'url'     => str_replace($this->site_cfg->upload_path, '', $destination),
            'name'    => basename($destination),
            'size'    => $image_size
        );

     * Загружает файл на сервер переданный через XHR
     * @param string $post_filename Название поля с файлом в массиве $_GET
     * @param string $allowed_ext Список допустимых расширений (через запятую)
     * @param string $allowed_size Максимальный размер файла (в байтах)
     * @param string $destination Папка назначения (внутри пути upload)
    public function uploadXHR($post_filename, $allowed_ext = false, $allowed_size = 0, $destination = false){
        $dest_name = files_sanitize_name($_GET['qqfile'], false);
        $dest_ext  = pathinfo($dest_name, PATHINFO_EXTENSION);
        if(!$this->checkExt($dest_ext, $allowed_ext)){
            return array(
                'error'   => LANG_UPLOAD_ERR_MIME,
            );
        if ($allowed_size){
            if ($this->getXHRFileSize() > $allowed_size){
                return array(
                );
        if (!$destination){
        return $this->saveXHRFile($destination, $dest_name);

    }

    public function getXHRFileSize(){
        if (isset($_SERVER["CONTENT_LENGTH"])){
            return (int)$_SERVER["CONTENT_LENGTH"];
        } else {
            return false;
        }
    }

//============================================================================//
//============================================================================//

    public function saveXHRFile($destination, $orig_name=''){

        $target = @fopen($destination, 'wb');
        $input  = @fopen("php://input", 'rb');

        if (!$target){
            return array(
                'name'    => $orig_name,
                'path'    => ''
            );
        }
        if (!$input){
            return array(
                'success' => false,
                'error'   => LANG_UPLOAD_ERR_NO_FILE,
                'name'    => $orig_name,
            );
        while ($buff = fread($input, 4096)) {
            fwrite($target, $buff);
        }

        @fclose($target);
        @fclose($input);

        $real_size = filesize($destination);

        if (!$real_size || $real_size != $this->getXHRFileSize()){
            @unlink($destination);
            return array(
                'error'   => LANG_UPLOAD_ERR_PARTIAL,
                'name'    => $orig_name,
            );
        if($this->allowed_mime !== false){
            if(!$this->isMimeTypeAllowed($destination)){
                @unlink($destination);
                return array(
                    'error'   => LANG_UPLOAD_ERR_MIME.'. '.sprintf(LANG_PARSER_FILE_EXTS_FIELD_HINT, implode(', ', $this->allowed_mime_ext)),
                    'success' => false,
                    'name'    => $orig_name,
                    'path'    => ''
                );
            }
        }

        return array(
            'name'    => $orig_name,
            'size'    => $real_size
        );

//============================================================================//
     * Копирует файл из временной папки в целевую и отслеживает ошибки
     * @return bool
    private function moveUploadedFile($source, $destination, $errorCode, $orig_name='', $orig_size=0){
        if($errorCode !== UPLOAD_ERR_OK && isset($this->upload_errors[$errorCode])){
            return array(
            );

        if (!is_writable($upload_dir)){	@chmod($upload_dir, 0777); }
        if (!is_writable($upload_dir)){
            return array(
            );
        return array(
        );

    public function remove($file_path){
        return @unlink($file_path);
    public function getUploadDestinationDirectory(){
    /**
     * Проверяет файл, является ли он изображением
     * @param string $src
     * @return boolean
     */
    public function isImage($src){

        $size = getimagesize($src);

        return $size !== false;

    }


Fixed code:
        $result = $this->cms_uploader->setAllowedMime([
            'application/zip'
        ])->upload($this->upload_name, $this->upload_exts, 0, $this->installer_upload_path);
    private $site_cfg = null;
    private $last_error = null;
    private $upload_errors = [];
    private $mime_types = [];
    public function __construct($user_id = null) {

        $this->mime_types = (new cmsConfigs('mimetypes.php'))->getAll();

        $this->upload_errors = [
        ];

        $this->user_id  = $user_id ?? cmsUser::getInstance()->id;
    /**
     * Устанавливает разрешённые типы файлов
     *
     * @param array $types
     * @return $this
     */
            if (isset($this->mime_types[$mime])) {
                $this->allowed_mime_ext[] = $this->mime_types[$mime];
    /**
     * Устанавливает разрешённые типы файлов по их расширению
     *
     * @param array|string $allowed_ext
     * @return $this
     */
    private function setAllowedMimeByExt($allowed_ext) {

        // Если установлено ранее, то ничего не делаем
        if ($this->allowed_mime) {
            return $this;
        }

        $this->allowed_mime = [];

        if (!is_array($allowed_ext)) {
            $allowed_ext = explode(',', (string) $allowed_ext);
        }

        foreach ($allowed_ext as $aext) {

            $aext = mb_strtolower(trim(trim((string) $aext, '., ')));

            if (empty($aext)) {
                continue;
            }

            $mime_key = array_search($aext, $this->mime_types, true);

            if(!$mime_key){
                continue;
            }

            $this->allowed_mime[] = $mime_key;

            $this->allowed_mime_ext[] = $aext;
        }

    }

    /**
     * Устанавливает имя файла
     *
     * @param string $name
     * @return $this
     */

        $this->file_name = mb_substr(trim($name), 0, 64);

        return $this;
    /**
     * Устанавливает id пользователя
     *
     * @param int $id
     * @return $this
     */

        $this->user_id = $id;

        return $this;
    /**
     * Возвращает последнюю ошибку
     *
     * @return ?string
     */
     *
    public function getMaxUploadSize() {
    /**
     * Проверяет, загружен ли файл наличием его в $_FILES
     *
     * @param string $name Имя в массиве $_FILES
     * @return boolean
     */
    public function isUploaded($name) {
        if (!isset($_FILES[$name])) {
            return false;
        }
            if (isset($_FILES[$name]['error'])) {
                if (isset($this->upload_errors[$_FILES[$name]['error']]) && $this->upload_errors[$_FILES[$name]['error']] !== UPLOAD_ERR_OK) {
    /**
     * Проверяет, загружен ли файл через XHR
     *
     * @param string $name Имя в массиве $_GET
     * @return boolean
     */
    public function isUploadedXHR($name) {
    /**
     * Проверяет, надо ли загрузить файл по ссылке
     *
     * @param string $name Имя в массиве $_POST
     * @return boolean
     */
    public function isUploadedFromLink($name) {
    /**
     * Разрешает загрузку по ссылке
     *
     * @return $this
     */
        $this->allow_remote = true;
        return $this;
     * Запрещает загрузку по ссылке
     *
     * @return $this
    public function disableRemoteUpload() {
        $this->allow_remote = false;
        return $this;
    /**
     * Возвращает имя файла с расширением
     * проверяя наличии одноимённого
     *
     * @param string $path Путь к директории хранения файла
     * @param string $file_ext Расширение файла
     * @param ?string $file_name Имя файла
     * @return string
     */
    private function getFileName($path, $file_ext, $file_name = null) {
        if (!$file_name) {
            if ($this->file_name) {
                $file_name = str_replace('.' . $file_ext, '', files_sanitize_name($this->file_name . '.' . $file_ext));
            } else {
                $file_name = substr(md5(microtime(true)), 0, 8);
        if (file_exists($path . $file_name . '.' . $file_ext)) {
            return $this->getFileName($path, $file_ext, $file_name . '_' . md5(microtime(true)));
        }
        return $file_name . '.' . $file_ext;
     *
     * @param string $filename Название поля с файлом
     * @param string $destination Директория назначения (внутри пути upload)
    public function upload($filename, $allowed_ext = false, $allowed_size = 0, $destination = false) {
        // Если переданы расширения
        if ($allowed_ext) {
            $this->setAllowedMimeByExt($allowed_ext);
        if ($this->isUploadedFromLink($filename)) {
            return $this->uploadFromLink($filename, $allowed_size, $destination);
        if ($this->isUploadedXHR($filename)) {
            return $this->uploadXHR($filename, $allowed_size, $destination);
        }

        if ($this->isUploaded($filename)) {
            return $this->uploadForm($filename, $allowed_size, $destination);
        return [
        ];
     *
     * @param string $filename Название поля с файлом в массиве $_FILES
     * @param int $allowed_size Максимальный размер файла (в байтах)
     * @param string $destination Директория назначения (внутри пути upload)
    public function uploadForm($filename, $allowed_size = 0, $destination = false) {
        $source     = $_FILES[$filename]['tmp_name'];
        $error_code = $_FILES[$filename]['error'];
        $dest_size  = (int) $_FILES[$filename]['size'];
        $dest_name  = files_sanitize_name($_FILES[$filename]['name']);
        $file = new cmsUploadfile($source, $this->allowed_mime);

        if (!$file->isAllowed()) {
            return [
                'error'   => LANG_UPLOAD_ERR_MIME . '. ' . sprintf(LANG_PARSER_FILE_EXTS_FIELD_HINT, implode(', ', $this->allowed_mime_ext)),
            ];
        if ($allowed_size) {
            if ($dest_size > $allowed_size) {
                return [
                ];
        $dest_ext = $file->getExt();

        if (!$destination) {
    /**
     * Загружает файл по ссылке
     *
     * @param string $post_filename Название поля с файлом в массиве $_POST
     * @param int $allowed_size Максимальный размер файла (в байтах)
     * @param string $destination Директория назначения (внутри пути upload)
     * @return array
     */
    public function uploadFromLink($post_filename, $allowed_size = 0, $destination = false) {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $link);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HEADER, true);
        curl_setopt($curl, CURLOPT_NOBODY, true);
        curl_setopt($curl, CURLOPT_TIMEOUT, 5);
        $headers = curl_exec($curl);
        curl_close($curl);

        $matches = [];
        if (preg_match("/(?:Location:|URI:)([^\n]+)*/is", $headers, $matches)) {

            $url = trim($matches[1]);

            if (strpos($url, 'http') !== 0) {
                $url_data = parse_url($link);
                $link     = $url_data['scheme'] . '://' . $url_data['host'] . $url;
            } else {
                $link = $url;
            $_POST[$post_filename] = $link;
            return $this->uploadFromLink($post_filename, $allowed_size, $destination);
        }

        if (preg_match('#filename="([^"]+)#uis', $headers, $matches)) {
            $file_name = trim($matches[1]);
        $dest_name = files_sanitize_name($file_name);

        if (!$file_bin) {
            return [
                'error'   => LANG_UPLOAD_ERR_NO_FILE,
            ];
        return $this->saveFileFromString($file_bin, $allowed_size, $destination, $dest_name);
    }
    /**
     * Сохраняет файл из php://input в целевую директорию  и отслеживает ошибки
     *
     * @param string $filename Название поля с файлом в массиве $_GET
     * @param int $allowed_size Максимальный размер файла (в байтах)
     * @param string $destination Директория назначения (внутри пути upload)
     * @return array
     */
    public function uploadXHR($filename, $allowed_size = 0, $destination = false) {
        $dest_name = files_sanitize_name($_GET['qqfile']);
        $file_bin = file_get_contents('php://input');
        if (!$file_bin) {
            return [
                'success' => false,
                'error'   => LANG_UPLOAD_ERR_NO_FILE,
                'name'    => $dest_name,
                'path'    => ''
            ];
        return $this->saveFileFromString($file_bin, $allowed_size, $destination, $dest_name);
     * Сохраняет файл из строки данных
     *
     * @param string $file_bin
     * @param int $allowed_size Максимальный размер файла (в байтах)
     * @param string $destination Директория назначения (внутри пути upload)
     * @param string $dest_name Имя файла
    private function saveFileFromString($file_bin, $allowed_size, $destination, $dest_name) {
        $file = new cmsUploadfile($file_bin, $this->allowed_mime);
        if (!$file->isAllowed()) {
            return [
                'error'   => LANG_UPLOAD_ERR_MIME . '. ' . sprintf(LANG_PARSER_FILE_EXTS_FIELD_HINT, implode(', ', $this->allowed_mime_ext)),
            ];
        $dest_ext = $file->getExt();

        $file_size = strlen($file_bin);

        if ($allowed_size) {
            if ($file_size > $allowed_size) {
                return [
                ];
        if (!$destination) {
        if (!is_writable(dirname($destination))) {
            return [
                'name'    => $dest_name,
            ];
        if(file_put_contents($destination, $file_bin) === false){
            return [
                'error'   => LANG_UPLOAD_ERR_CANT_WRITE,
                'name'    => $dest_name,
            ];
        return [
            'name'    => basename($destination),
            'size'    => $file_size
        ];
     * Копирует файл из временной директории в целевую и отслеживает ошибки
     *
     * @return array
    private function moveUploadedFile($source, $destination, $errorCode, $orig_name = '', $orig_size = 0) {
        if ($errorCode !== UPLOAD_ERR_OK && isset($this->upload_errors[$errorCode])) {
            return [
            ];
        if (!is_writable($upload_dir)) {
            @chmod($upload_dir, 0777);
        }

        if (!is_writable($upload_dir)) {
            return [
            ];
        return [
        ];
    public function remove($file_path) {
        return files_delete_file($file_path, 2);
    public function getUploadDestinationDirectory() {
<?php

class cmsUploadfile {

    private $file_mime;
    private $mime_types;
    private $allowed_mime;

    public function __construct($file_path, $allowed_mime = null) {

        $this->allowed_mime = $allowed_mime;

        $this->mime_types = (new cmsConfigs('mimetypes.php'))->getAll();

        $finfo = finfo_open(FILEINFO_MIME_TYPE);

        if(strpos($file_path, DIRECTORY_SEPARATOR) === 0){

            $this->file_mime = finfo_file($finfo, $file_path);

        } else {

            $this->file_mime = finfo_buffer($finfo, $file_path);
        }

        finfo_close($finfo);
    }

    public function getMime() {
        return $this->file_mime;
    }

    public function getExt() {

        if ($this->file_mime && isset($this->mime_types[$this->file_mime])) {

            return $this->mime_types[$this->file_mime];
        }

        return 'bin';
    }

    public function isAllowed() {

        // Пускаем любые, если не указано
        if ($this->allowed_mime === null) {
            return true;
        }

        if ($this->file_mime === false) {
            return false;
        }

        return in_array($this->file_mime, $this->allowed_mime, true);
    }

}