diff --git a/.gitignore b/.gitignore index dbb8db2017..f1e8cf5613 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ ftpsync.settings Thumbs.db Desktop.ini +# SQLite database +/core/database/*.sqlite + # all dotfiles and dotfolders except do not ignore .gitignore **/.* !.gitignore diff --git a/core/functions/actions/files.php b/core/functions/actions/files.php index e2fb6d2b20..b4673fec47 100644 --- a/core/functions/actions/files.php +++ b/core/functions/actions/files.php @@ -73,6 +73,10 @@ function ls($curpath, array $options = []) { extract($options, EXTR_OVERWRITE); + $curpath = rtrim(str_replace('\\', '/', $curpath), '/') . '/'; + $filemanager_path = rtrim(str_replace('\\', '/', $filemanager_path), '/'); + $base_path = rtrim(str_replace('\\', '/', $base_path), '/'); + $_lang = ManagerTheme::getLexicon(); $_style = ManagerTheme::getStyle(); $dircounter = 0; @@ -80,10 +84,9 @@ function ls($curpath, array $options = []) $filesizes = 0; $dirs_array = []; $files_array = []; - $curpath = str_replace('//', '/', $curpath . '/'); if (!is_dir($curpath)) { - echo 'Invalid path "', $curpath, '"
'; + echo 'Invalid path "', htmlspecialchars($curpath, ENT_QUOTES, 'UTF-8'), '"
'; return; } @@ -95,13 +98,17 @@ function ls($curpath, array $options = []) if ($file === '..' || $file === '.') { continue; } + $rel_newpath = ltrim(substr($newpath, strlen($filemanager_path)), '/'); + $rel_web = ltrim(substr($newpath, strlen($base_path)), '/'); if (is_dir($newpath)) { $dirs_array[$dircounter]['dir'] = $newpath; $dirs_array[$dircounter]['stats'] = lstat($newpath); if ($file === '..' || $file === '.') { continue; } elseif (!in_array($file, $excludes) && !in_array($newpath, $protected_path)) { - $dirs_array[$dircounter]['text'] = ' ' . $file . ''; + $dirs_array[$dircounter]['text'] = ' ' + . '' + . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . ''; $dfiles = scandir($newpath); foreach ($dfiles as $i => $infile) { @@ -114,38 +121,70 @@ function ls($curpath, array $options = []) } $file_exists = (0 < count($dfiles)) ? 'file_exists' : ''; - $dirs_array[$dircounter]['delete'] = is_writable($curpath) ? '' : ''; + $dirs_array[$dircounter]['delete'] = is_writable($curpath) ? '' : ''; } else { - $dirs_array[$dircounter]['text'] = ' ' . $file . ''; - $dirs_array[$dircounter]['delete'] = is_writable($curpath) ? '' : ''; + $dirs_array[$dircounter]['text'] = ' ' . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') + . ''; + $dirs_array[$dircounter]['delete'] = is_writable($curpath) ? '' : ''; } - $dirs_array[$dircounter]['rename'] = is_writable($curpath) ? ' ' : ''; + $dirs_array[$dircounter]['rename'] = is_writable($curpath) ? ' ' : ''; // increment the counter $dircounter++; } else { $type = getExtension($newpath); - $files_array[$filecounter]['file'] = $newpath; + $files_array[$filecounter]['file'] = $rel_newpath; $files_array[$filecounter]['stats'] = lstat($newpath); - $files_array[$filecounter]['text'] = determineIcon($newpath, get_by_key($_REQUEST, 'path', ''), get_by_key($_REQUEST, 'mode', '')) . ' ' . $file; - $files_array[$filecounter]['view'] = (in_array($type, - $viewablefiles)) ? '' : (($enablefiledownload && in_array($type, - $uploadablefiles)) ? '' : ''); - $files_array[$filecounter]['view'] = (in_array($type, - $inlineviewablefiles)) ? '' : $files_array[$filecounter]['view']; - $files_array[$filecounter]['unzip'] = ($enablefileunzip && $type == '.zip') ? '' : ''; + $files_array[$filecounter]['text'] = determineIcon($rel_newpath, get_by_key($_REQUEST, 'path', ''), + get_by_key($_REQUEST, 'mode', '')) . ' ' . htmlspecialchars($file, ENT_QUOTES, + 'UTF-8'); + $files_array[$filecounter]['view'] = in_array($type, $viewablefiles) + ? '' + : (($enablefiledownload && in_array($type, $uploadablefiles)) + ? '' : ''); + $files_array[$filecounter]['view'] = (in_array($type, $inlineviewablefiles)) + ? '' + : $files_array[$filecounter]['view']; + $files_array[$filecounter]['unzip'] = ($enablefileunzip && $type == '.zip') + ? '' + : ''; $files_array[$filecounter]['edit'] = (in_array($type, - $editablefiles) && is_writable($curpath) && is_writable($newpath)) ? '' : ''; - $files_array[$filecounter]['duplicate'] = (in_array($type, - $editablefiles) && is_writable($curpath) && is_writable($newpath)) ? '' : ''; - $files_array[$filecounter]['rename'] = (in_array($type, - $editablefiles) && is_writable($curpath) && is_writable($newpath)) ? '' : ''; - $files_array[$filecounter]['delete'] = is_writable($curpath) && is_writable($newpath) ? '' : ''; + $editablefiles) && is_writable($curpath) && is_writable($newpath)) + ? '' + : ''; + $files_array[$filecounter]['duplicate'] = (in_array($type, $editablefiles) && is_writable($curpath) + && is_writable($newpath)) + ? '' + : ''; + $files_array[$filecounter]['rename'] = (in_array($type, $editablefiles) && is_writable($curpath) + && is_writable($newpath)) + ? '' + : ''; + $files_array[$filecounter]['delete'] = is_writable($curpath) && is_writable($newpath) + ? '' + : ''; // increment the counter $filecounter++; @@ -333,37 +372,43 @@ function unzip($file, $path) return 0; } // end mod - $zip = zip_open($file); - if ($zip) { - $old_umask = umask(0); - $path = rtrim($path, '/') . '/'; - while ($zip_entry = zip_read($zip)) { - if (zip_entry_filesize($zip_entry) > 0) { - // str_replace must be used under windows to convert "/" into "\" - $zip_entry_name = zip_entry_name($zip_entry); - $complete_path = $path . str_replace('\\', '/', dirname($zip_entry_name)); - $complete_name = $path . str_replace('\\', '/', $zip_entry_name); - if (!file_exists($complete_path)) { - $tmp = ''; - foreach (explode('/', $complete_path) AS $k) { - $tmp .= $k . '/'; - if (!is_dir($tmp)) { - mkdir($tmp, 0777); - } - } - } - if (zip_entry_open($zip, $zip_entry, 'r')) { - file_put_contents($complete_name, zip_entry_read($zip_entry, zip_entry_filesize($zip_entry))); - zip_entry_close($zip_entry); - } + + $old_umask = umask(0); + $path = rtrim(str_replace('\\', '/', realpath($path)), '/\\'); // No trailing slash + + $zip = new ZipArchive(); + if ($zip->open($file) !== true) { + umask($old_umask); + return false; + } + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + $filename = str_replace('\\', '/', $stat['name']); + if (substr($filename, 0, 1) == '/' || strpos($filename, '..') !== false || + strpos($filename, ':') !== false) { + continue; // skip malicious paths + } + $target = $path . '/' . $filename; + // Additional check to ensure target is within path + $target_dir = rtrim(str_replace('\\', '/', realpath(dirname($target)) ?: dirname($target)), '/\\'); + if (strpos($target_dir, $path) !== 0) { + continue; + } + if (substr($filename, -1) == '/') { + if (!is_dir($target)) { + mkdir($target, $newfolderaccessmode ?: 0777, true); } + } else { + $dirname = dirname($target); + if (!is_dir($dirname)) { + mkdir($dirname, $newfolderaccessmode ?: 0777, true); + } + file_put_contents($target, $zip->getFromIndex($i)); } - umask($old_umask); - zip_close($zip); - - return true; } - zip_close($zip); + $zip->close(); + umask($old_umask); + return true; } } @@ -374,6 +419,7 @@ function unzip($file, $path) */ function rrmdir($dir) { + $dir = str_replace('\\', '/', realpath($dir)); // Canonicalize path foreach (glob($dir . '/*') as $file) { if (is_dir($file)) { rrmdir($file); @@ -393,8 +439,14 @@ function rrmdir($dir) function fileupload() { $modx = evolutionCMS(); - $startpath = is_dir($_REQUEST['path']) ? $_REQUEST['path'] : removeLastPath($_REQUEST['path']); - $filemanager_path = evolutionCMS()->getConfig('filemanager_path', MODX_BASE_PATH); + $filemanager_path = rtrim(str_replace('\\', '/', realpath(evolutionCMS()->getConfig('filemanager_path', MODX_BASE_PATH))), '/'); // Canonicalize base path + $requested_path = ltrim($_REQUEST['path'] ?? '', '/'); + $startpath = str_replace('\\', '/', realpath($filemanager_path . '/' . $requested_path)); + $startpath = rtrim($startpath, '/'); + // Ensure startpath is within filemanager_path + if (strpos($startpath, $filemanager_path) !== 0 || !is_dir($startpath)) { + return '

Invalid path.

'; + } $new_file_permissions = octdec(evolutionCMS()->getConfig('new_file_permissions', '0666')); global $_lang, $uploadablefiles; $msg = ''; @@ -415,17 +467,21 @@ function fileupload() ], $nameparts, ['file_manager']); $name = implode('.', $nameparts); } + // Sanitize name to prevent traversal or invalid chars + $name = preg_replace('/[^\w\.-]/', '', $name); + $name = ltrim($name, '.'); $userfile['name'] = $name; $userfile['type'] = $_FILES['userfile']['type'][$i]; // this seems to be an upload action. - $path = MODX_SITE_URL . substr($startpath, strlen($filemanager_path), strlen($startpath)); - $path = rtrim($path, '/') . '/' . $userfile['name']; - $msg .= $path; + $rel_path = ltrim(substr($startpath, strlen($filemanager_path)), '/'); + $path = MODX_SITE_URL . ($rel_path ? $rel_path . '/' : '') . $userfile['name']; + $msg .= htmlspecialchars($path, ENT_QUOTES, 'UTF-8'); if ($userfile['error'] == 0) { - $img = (strpos($userfile['type'], - 'image') !== false) ? '
' : ''; - $msg .= "

" . $_lang['files_file_type'] . $userfile['type'] . ", " . niceSize(filesize($userfile['tmp_name'])) . $img . '

'; + $img = (strpos($userfile['type'],'image') !== false) ? '
' : ''; + $msg .= "

" . $_lang['files_file_type'] . htmlspecialchars($userfile['type'], ENT_QUOTES, + 'UTF-8') . ", " . niceSize(filesize($userfile['tmp_name'])) . $img . '

'; } $userfilename = $userfile['tmp_name']; @@ -435,23 +491,25 @@ function fileupload() if (!checkExtension($userfile['name'])) { $msg .= '

' . $_lang['files_filetype_notok'] . '

'; } else { - if (@move_uploaded_file($userfile['tmp_name'], $_POST['path'] . '/' . $userfile['name'])) { + $targetFile = $startpath . '/' . $userfile['name']; + if (@move_uploaded_file($userfile['tmp_name'], $targetFile)) { // Ryan: Repair broken permissions issue with file manager if (strtoupper(substr(PHP_OS, 0, 3)) != 'WIN') { - @chmod($_POST['path'] . "/" . $userfile['name'], $new_file_permissions); + @chmod($targetFile, $new_file_permissions); } // Ryan: End $msg .= '

' . $_lang['files_upload_ok'] . '


'; // invoke OnFileManagerUpload event $modx->invokeEvent('OnFileManagerUpload', [ - 'filepath' => $_POST['path'], + 'filepath' => $startpath, 'filename' => $userfile['name'] ]); // Log the change - logFileChange('upload', $_POST['path'] . '/' . $userfile['name']); + logFileChange('upload', $targetFile); } else { - $msg .= '

' . $_lang['files_upload_copyfailed'] . ' ' . $_lang["files_upload_permissions_error"] . '

'; + $msg .= '

' . $_lang['files_upload_copyfailed'] . ' ' + . $_lang["files_upload_permissions_error"] . '

'; } } } else { @@ -492,15 +550,19 @@ function textsave() { global $_lang; - $msg = $_lang['editing_file']; - $filename = $_POST['path']; + $filemanager_path = rtrim(str_replace('\\', '/', realpath(evolutionCMS()->getConfig('filemanager_path', MODX_BASE_PATH))), '/'); + $requested_path = ltrim($_POST['path'] ?? '', '/'); + $filename = str_replace('\\', '/', realpath($filemanager_path . '/' . $requested_path)); + if (strpos($filename, $filemanager_path) !== 0 || !is_file($filename)) { + return 'Invalid path.

'; + } $content = $_POST['content']; // Write $content to our opened file. if (file_put_contents($filename, $content) === false) { - $msg .= '' . $_lang['file_not_saved'] . '

'; + $msg = '' . $_lang['file_not_saved'] . '

'; } else { - $msg .= '' . $_lang['file_saved'] . '

'; + $msg = '' . $_lang['file_saved'] . '

'; $_REQUEST['mode'] = 'edit'; } // Log the change @@ -518,9 +580,14 @@ function delete_file() { global $_lang; - $msg = sprintf($_lang['deleting_file'], str_replace('\\', '/', $_REQUEST['path'])); + $filemanager_path = rtrim(str_replace('\\', '/', realpath(evolutionCMS()->getConfig('filemanager_path', MODX_BASE_PATH))), '/'); + $requested_path = ltrim($_REQUEST['path'] ?? '', '/'); + $file = str_replace('\\', '/', realpath($filemanager_path . '/' . $requested_path)); + if (strpos($file, $filemanager_path) !== 0 || !is_file($file)) { + return 'Invalid path.

'; + } + $msg = sprintf($_lang['deleting_file'], str_replace('\\', '/', $file)); - $file = $_REQUEST['path']; if (!evolutionCMS()->hasPermission('file_manager') || !@unlink($file)) { $msg .= '' . $_lang['file_not_deleted'] . '

'; } else { @@ -582,7 +649,7 @@ function checkToken() */ function makeToken() { - $newToken = uniqid(''); + $newToken = uniqid('', true); $_SESSION['token'] = $newToken; return $newToken; diff --git a/install/index.php b/install/index.php index d32c5010a5..4ab0a6fe5b 100644 --- a/install/index.php +++ b/install/index.php @@ -138,7 +138,11 @@ if (! file_exists($controller)) { die("Invalid install action attempted. [action={$action}]"); } - require $controller; + try { + require $controller; + } catch (Exception $e) { + echo $e->getMessage(); + } $ph['content'] = ob_get_contents(); ob_end_clean(); diff --git a/install/src/controllers/connection.php b/install/src/controllers/connection.php index 9068104d25..6f920b487b 100644 --- a/install/src/controllers/connection.php +++ b/install/src/controllers/connection.php @@ -1,4 +1,5 @@ 'MySQL', 'pgsql' => 'PostgreSQL', 'sqlite' => 'SQLite']; @@ -72,7 +73,7 @@ } } if (!$conn || !$result) { - $upgradeable = (isset($_POST['installmode']) && $_POST['installmode'] === 'new') ? 0 : 2; + $upgradeable = ($installMode === 0) ? 0 : 2; } else { $upgradeable = 1; } @@ -143,7 +144,7 @@ $ph['database_name'] = escapeHtmlAttribute(isset($_POST['database_name']) ? $_POST['database_name'] : $database_name); $ph['tableprefix'] = escapeHtmlAttribute(isset($_POST['tableprefix']) ? $_POST['tableprefix'] : $table_prefix); $ph['database_collation'] = escapeHtmlAttribute(isset($_POST['database_collation']) ? $_POST['database_collation'] : $database_collation); -$ph['show#AUH'] = ($installMode == 0) ? '' : 'hidden'; +$ph['show#AUH'] = ($installMode === 0) ? '' : 'hidden'; $ph['cmsadmin'] = escapeHtmlAttribute(isset($_POST['cmsadmin']) ? $_POST['cmsadmin'] : 'admin'); $ph['cmsadminemail'] = escapeHtmlAttribute(isset($_POST['cmsadminemail']) ? $_POST['cmsadminemail'] : ''); $ph['cmspassword'] = escapeHtmlAttribute(isset($_POST['cmspassword']) ? $_POST['cmspassword'] : ''); diff --git a/install/src/controllers/connection/collation.php b/install/src/controllers/connection/collation.php index 55c71ae7d2..bb6241b5c9 100644 --- a/install/src/controllers/connection/collation.php +++ b/install/src/controllers/connection/collation.php @@ -1,10 +1,14 @@ ' . $_lang['status_failed'] . ' ' . $e->getMessage() . ''; +} try { $dsn = $driver . ':host=' . $host; $output = ' - - - - + + + +
- " . $_lang['files_upload_inhibited_msg'] . "

"; - } - ?> + } ?>
- - - +
webAlertAndQuit("Invalid path."); + } $buffer = file_get_contents($filename); // Log the change logFileChange('view', $filename); @@ -487,7 +522,8 @@ function renameFile(file) {
- + +
@@ -516,15 +552,12 @@ function renameFile(file) { $contentType = 'htmlmixed'; }; $evtOut = evo()->invokeEvent('OnRichTextEditorInit', [ - 'editor' => 'Codemirror', - 'elements' => [ - 'content', - ], - 'contentType' => $contentType, - 'readOnly' => $_REQUEST['mode'] == 'edit' ? false : true + 'editor' => 'Codemirror', + 'elements' => ['content'], + 'contentType' => $contentType, + 'readOnly' => $_REQUEST['mode'] !== 'edit' ]); if (is_array($evtOut)) { echo implode('', $evtOut); } } -