From 751874ea8c34ed5f98f9b488d4b0911bc692f273 Mon Sep 17 00:00:00 2001 From: iaretechnician Date: Wed, 10 Jun 2026 18:59:12 -0400 Subject: [PATCH] liteFM fixes --- Panel/js/modules/litefm.js | 31 +++-- Panel/modules/ftp/monitor_buttons.php | 3 + Panel/modules/litefm/fm_dir.php | 191 ++++++++++++++++++-------- Panel/modules/litefm/litefm.php | 113 ++++++++++++--- docs/modules/litefm.md | 26 +++- 5 files changed, 280 insertions(+), 84 deletions(-) diff --git a/Panel/js/modules/litefm.js b/Panel/js/modules/litefm.js index 32f10bec..9de261a6 100644 --- a/Panel/js/modules/litefm.js +++ b/Panel/js/modules/litefm.js @@ -234,7 +234,7 @@ function downloadFile(home_id, file_id, file_size, file_name) function getFilePart() { var xhr = new XMLHttpRequest(); - xhr.open('POST', "home.php?m=litefm&home_id="+home_id+"&item="+file_id+"&name="+file_name+"&p=get&type=cleared&size="+file_size+"&did="+did, true); + xhr.open('POST', "home.php?m=litefm&home_id="+home_id+"&item="+file_id+"&name="+encodeURIComponent(file_name)+"&p=get&type=cleared&size="+file_size+"&did="+did, true); xhr.responseType = 'arraybuffer'; xhr.onload = function(e) { if(this.response.byteLength != 0) @@ -283,6 +283,15 @@ function downloadFile(home_id, file_id, file_size, file_name) } $(document).ready(function(){ + function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + /* translation & info */ var select_at_least_one_item = $('#dialog').attr('data-select_at_least_one_item'), ask_delete = $('#dialog').attr('data-ask_delete'), @@ -389,7 +398,7 @@ $(document).ready(function(){ item = $(this).attr('data-item'); value = $(this).attr('value'); addpost.items.push(item); - items += "
"+value; + items += "
"+escapeHtml(value); }); if(items != '') { @@ -434,9 +443,9 @@ $(document).ready(function(){ var items = ''; $('input[class="item"]:checked').each(function(){ item = $(this).attr('data-item'); - value = $(this).attr('value').replace('"', """); + value = $(this).attr('value'); addpost.items.push(item); - items += "
"; + items += "
"; }); if(items != '') { @@ -487,7 +496,7 @@ $(document).ready(function(){ item = $(this).attr('data-item'); value = $(this).attr('value'); addpost.items.push(item); - items += "
"+value; + items += "
"+escapeHtml(value); }); if(items != '') { @@ -550,7 +559,7 @@ $(document).ready(function(){ item = $(this).attr('data-item'); value = $(this).attr('value'); addpost.items.push(item); - items += "
"+value; + items += "
"+escapeHtml(value); }); if(items != '') { @@ -611,7 +620,7 @@ $(document).ready(function(){ item = $(this).attr('data-item'); value = $(this).attr('value'); addpost.items.push(item); - items += "
"+value; + items += "
"+escapeHtml(value); }); if(items != '') { @@ -675,7 +684,7 @@ $(document).ready(function(){ if(ext.match(/^(tar|tgz|gz|Z|zip|bz2|tbz|lzma|xz|txz)$/i) != null) { addpost.items.push(item); - items += "
"+value; + items += "
"+escapeHtml(value); } }); if(items != '') @@ -902,7 +911,7 @@ $(document).ready(function(){ } $.ajax({ type: "POST", - url: 'home.php?m=litefm&home_id='+home_id+'&type=cleared&pid='+file['pid']+'&size='+file['size']+'&filename='+file['filename']+"&data_type=json", + url: 'home.php?m=litefm&home_id='+home_id+'&type=cleared&pid='+file['pid']+'&size='+file['size']+'&filename='+encodeURIComponent(file['filename'])+"&data_type=json", dataType:'json', async: false, success: function(data){ @@ -978,7 +987,7 @@ $(document).ready(function(){ addpost.set_attr = $(this).attr('data-set_attr'), addpost.file_name = $(this).attr('data-file_name'); addpost.item = $(this).attr('data-item'); - $('#dialog').html(ask_change_attr.replace("%file_name%",addpost.file_name)); + $('#dialog').html(ask_change_attr.replace("%file_name%",escapeHtml(addpost.file_name))); $('#dialog').dialog({ autoOpen: true, width: 450, @@ -1016,7 +1025,7 @@ $(document).ready(function(){ item = $(this).attr('data-item'); value = $(this).attr('value'); addpost.items.push(item); - items += "
"+value; + items += "
"+escapeHtml(value); }); if(items != '') { diff --git a/Panel/modules/ftp/monitor_buttons.php b/Panel/modules/ftp/monitor_buttons.php index 21b3c639..89d92e0b 100644 --- a/Panel/modules/ftp/monitor_buttons.php +++ b/Panel/modules/ftp/monitor_buttons.php @@ -27,6 +27,9 @@ if(preg_match("/t/",$server_home['access_rights']) > 0) { $module_buttons = array(); + // WE DONT WANT A FTP BUTTON, BUT WE DO NEED THE FTP FUNCTIONALITY, SO WE WILL NOT ADD A FTP BUTTON TO THE GAME MONITOR, BUT WE WILL STILL ALLOW THE USER TO ACCESS THE FTP MODULE FOR THIS HOME + return; + $module_buttons = array( " diff --git a/Panel/modules/litefm/fm_dir.php b/Panel/modules/litefm/fm_dir.php index d2862bb6..724b5945 100644 --- a/Panel/modules/litefm/fm_dir.php +++ b/Panel/modules/litefm/fm_dir.php @@ -68,30 +68,29 @@ function exec_ogp_module() if (!isset($_SESSION[$cwd_session_key]) || !is_string($_SESSION[$cwd_session_key])) { $_SESSION[$cwd_session_key] = ''; } - $path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]); + $path = litefm_safe_join_home_path($home_cfg['home_path'], $_SESSION[$cwd_session_key]); + if ($path === false) + { + print_failure(get_lang("unallowed_char")); + echo "
<< ".get_lang('back')."
"; + return; + } + + $home_root = clean_path($home_cfg['home_path']); + if (!$remote->rfile_exists($home_root)) + { + print_failure("Server files have not been installed yet."); + echo "
<< ".get_lang('back')."
"; + return; + } + if (!$remote->rfile_exists($path)) { - while(!$remote->rfile_exists($path)) - { - $current_cwd = isset($_SESSION[$cwd_session_key]) ? (string)$_SESSION[$cwd_session_key] : ''; - if ($current_cwd === '' || $current_cwd === '.' || $current_cwd === DIRECTORY_SEPARATOR) { - print_failure("Server files have not been installed yet."); - echo "
<< ".get_lang('back')."
"; - return; - } - $parent_cwd = dirname($current_cwd); - if (!is_string($parent_cwd) || $parent_cwd === '.' || $parent_cwd === DIRECTORY_SEPARATOR) { - $parent_cwd = ''; - } - $_SESSION[$cwd_session_key] = $parent_cwd; - $path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]); - if($path == clean_path($home_cfg['home_path']."/")) - { - print_failure("Server files have not been installed yet."); - echo "
<< ".get_lang('back')."
"; - return; - } - } + $requested_rel_path = isset($_SESSION[$cwd_session_key]) ? trim((string)$_SESSION[$cwd_session_key], '/') : ''; + $requested_rel_path = $requested_rel_path === '' ? '/' : $requested_rel_path; + print_failure("Directory not found: " . litefm_escape_html($requested_rel_path)); + echo "
<< ".get_lang('back')."
"; + return; } // Get File Operations Keys @@ -112,7 +111,12 @@ function exec_ogp_module() { $bytes = $_GET['size']; $totalsize = $bytes / 1024; - $filename = $_GET['filename']; + $filename = litefm_decode_name_param($_GET['filename']); + if (!litefm_is_valid_path_component($filename)) + { + echo json_encode(array('pct' => 0, 'complete' => false)); + return; + } $kbytes = $remote->rsync_progress( clean_path( $path."/".$filename ) ); list($totalsize,$mbytes,$pct) = explode(";",do_progress($kbytes,$totalsize)); $totalmbytes = round($totalsize / 1024, 2); @@ -177,10 +181,9 @@ function exec_ogp_module() // if file not uploaded then skip it if ( !is_uploaded_file($_FILES['files']['tmp_name'][$i]) ) continue; - // now we can move uploaded files - $bad_chars = preg_replace( "/([[:alnum:]_\.-]*)/", "", $_FILES['files']['name'][$i] ); - $bad_arr = str_split( $bad_chars ); - $filename = str_replace( $bad_arr, "", $_FILES['files']['name'][$i] ); + $filename = basename((string)$_FILES['files']['name'][$i]); + if (!litefm_is_valid_path_component($filename)) + continue; $dest_file_path = clean_path( $upload_folder_path . "/" . $filename . ".txt" ); $file_url = str_replace( "home.php", $dest_file_path, $url ); if( file_exists( $dest_file_path ) ) @@ -205,7 +208,17 @@ function exec_ogp_module() elseif( isset( $_POST['create_folder'] ) and $fo['create_folder'] == "1" ) { $folder_name = stripslashes($_POST['folder_name']); + if (!litefm_is_valid_path_component($folder_name)) + { + print_failure(get_lang("unallowed_char")); + return; + } $folder_path = clean_path( $path . "/" . $folder_name ); + if (!litefm_path_within_home($home_cfg['home_path'], $folder_path)) + { + print_failure(get_lang("unallowed_char")); + return; + } $remote->shell_action('create_dir', $folder_path); $db->logger( get_lang("create_folder") . ": " . $folder_path ); } @@ -242,8 +255,18 @@ function exec_ogp_module() if(isset($_SESSION['fm_files_'.$home_id][$item])) { $item_path = clean_path( $path . "/" . $_SESSION['fm_files_'.$home_id][$item] ); - $new_item = removeInvalidFileNameCharacters(stripslashes($_POST['values'][$i])); + $new_item = stripslashes($_POST['values'][$i]); + if (!litefm_is_valid_path_component($new_item)) + { + print_failure(get_lang("unallowed_char")); + continue; + } $new_item_path = clean_path( $path . "/" . $new_item ); + if (!litefm_path_within_home($home_cfg['home_path'], $new_item_path)) + { + print_failure(get_lang("unallowed_char")); + continue; + } if ($item_path != $new_item_path) { $remote->shell_action('rename', "$item_path;$new_item_path"); @@ -256,8 +279,18 @@ function exec_ogp_module() // Move Files/Folders elseif( isset( $_POST['move'] ) and $fo['move'] == "1" ) { - $selected_path = preg_replace("#[/\.\./]+#","/", stripslashes($_POST['selected_path'])); - $destination = clean_path($home_cfg['home_path']. "/" . $selected_path); + $selected_path = litefm_normalize_relative_path(stripslashes($_POST['selected_path'])); + if ($selected_path === false) + { + print_failure(get_lang("unallowed_char")); + return; + } + $destination = litefm_safe_join_home_path($home_cfg['home_path'], $selected_path); + if ($destination === false) + { + print_failure(get_lang("unallowed_char")); + return; + } if($path != $destination) { if($remote->rfile_exists($destination)) @@ -278,8 +311,18 @@ function exec_ogp_module() // Copy Files/Folders elseif( isset( $_POST['copy'] ) and $fo['copy'] == "1" ) { - $selected_path = preg_replace("#[/\.\./]+#","/", stripslashes($_POST['selected_path'])); - $destination = clean_path($home_cfg['home_path']. "/" . $selected_path); + $selected_path = litefm_normalize_relative_path(stripslashes($_POST['selected_path'])); + if ($selected_path === false) + { + print_failure(get_lang("unallowed_char")); + return; + } + $destination = litefm_safe_join_home_path($home_cfg['home_path'], $selected_path); + if ($destination === false) + { + print_failure(get_lang("unallowed_char")); + return; + } if($path != $destination) { if($remote->rfile_exists($destination)) @@ -320,8 +363,18 @@ function exec_ogp_module() // uncompress elseif( isset( $_POST['uncompress'] ) and $fo['uncompress'] == "1" ) { - $selected_path = preg_replace("#[/\.\./]+#","/", stripslashes($_POST['selected_path'])); - $destination = clean_path($home_cfg['home_path']. "/" . $selected_path); + $selected_path = litefm_normalize_relative_path(stripslashes($_POST['selected_path'])); + if ($selected_path === false) + { + print_failure(get_lang("unallowed_char")); + return; + } + $destination = litefm_safe_join_home_path($home_cfg['home_path'], $selected_path); + if ($destination === false) + { + print_failure(get_lang("unallowed_char")); + return; + } if($remote->rfile_exists($destination)) { foreach ((array)$_POST['items'] as $item) @@ -338,8 +391,20 @@ function exec_ogp_module() // Create file elseif( isset( $_POST['create_file'] ) and $fo['create_file'] == "1" ) { - $file_name = removeInvalidFileNameCharacters(stripslashes($_POST['file_name'])); + $file_name = stripslashes($_POST['file_name']); + if (!litefm_is_valid_path_component($file_name)) + { + print_failure(get_lang("unallowed_char")); + $file_name = ""; + } + if ($file_name === "") + return; $destination = clean_path( $path . "/" . $file_name); + if (!litefm_path_within_home($home_cfg['home_path'], $destination)) + { + print_failure(get_lang("unallowed_char")); + return; + } $remote->shell_action('touch', $destination); $db->logger( get_lang("create_file") . ": $destination" ); } @@ -437,7 +502,12 @@ function exec_ogp_module() if (!is_array($dirlist)) { - if(isset($_SESSION['fm_cwd_'.$home_id])) + if ($remote->rfile_exists($path)) + { + print_failure("Directory exists but cannot be opened. Check permissions."); + return; + } + elseif(isset($_SESSION['fm_cwd_'.$home_id])) { unset($_SESSION['fm_cwd_'.$home_id]); $view->refresh("?m=litefm&home_id=$home_id",0); @@ -475,21 +545,25 @@ function exec_ogp_module() $dirlist['directorys'] = array_orderby($dirlist['directorys'], 'filename', SORT_ASC); foreach ((array)$dirlist['directorys'] as $directory) { - $directory['filename'] = removeInvalidFileNameCharacters($directory['filename']); + $directoryName = $directory['filename']; + if (!litefm_is_valid_path_component($directoryName)) + continue; + $encodedDirectoryName = rawurlencode($directoryName); + $escapedDirectoryName = litefm_escape_html($directoryName); echo "\n". "". - "\n". + "\n". "". "". "\"Directory\" ". - "
". - $directory['filename'] . ""; + "". + $escapedDirectoryName . ""; if( $os == "linux" ) echo "-"; echo "- " . $directory['user'] . " " . $directory['group']. "\n". "\n"; - $_SESSION['fm_files_'.$home_id][$i] = $directory['filename']; + $_SESSION['fm_files_'.$home_id][$i] = $directoryName; $i++; } } @@ -499,12 +573,16 @@ function exec_ogp_module() $dirlist['files'] = array_orderby($dirlist['files'], 'filename', SORT_ASC); foreach ((array)$dirlist['files'] as $file) { - $file['filename'] = removeInvalidFileNameCharacters($file['filename']); + $fileName = $file['filename']; + if (!litefm_is_valid_path_component($fileName)) + continue; + $encodedFileName = rawurlencode($fileName); + $escapedFileName = litefm_escape_html($fileName); if( $os == "linux" ) { if($isAdmin){ - $secureFile = "
\n". "". - "\n". + "\n". "". ""; echo "\"Text ". - "". get_lang("button_edit") ."". - "" .$file['filename'] . " ". + "". get_lang("button_edit") ."". + "" .$escapedFileName . " ". "$secureFile" . $file['size'] . " " . $file['user'] . " " . $file['group']. "\n"; echo "\n"; - $_SESSION['fm_files_'.$home_id][$i] = $file['filename']; + $_SESSION['fm_files_'.$home_id][$i] = $fileName; $i++; } } @@ -544,12 +622,15 @@ function exec_ogp_module() $dirlist['binarys'] = array_orderby($dirlist['binarys'], 'filename', SORT_ASC); foreach ((array)$dirlist['binarys'] as $binary) { - $binary['filename'] = removeInvalidFileNameCharacters($binary['filename']); + $binaryName = $binary['filename']; + if (!litefm_is_valid_path_component($binaryName)) + continue; + $escapedBinaryName = litefm_escape_html($binaryName); if( $os == "linux" ) { if($isAdmin){ - $secureFile = "
\n". "". - "\n". + "\n". "". ""; echo "\"Binary ". - "" .$binary['filename'] . " ". + "" .$escapedBinaryName . " ". "$secureFile" . $binary['size'] . "" . $binary['user'] . " " . $binary['group']. "\n"; echo "\n"; - $_SESSION['fm_files_'.$home_id][$i] = $binary['filename']; + $_SESSION['fm_files_'.$home_id][$i] = $binaryName; $i++; } } @@ -589,7 +670,7 @@ function exec_ogp_module() // Dialog translation && info $user = $db->getUserById($_SESSION['user_id']); echo "
"; } } diff --git a/Panel/modules/litefm/litefm.php b/Panel/modules/litefm/litefm.php index c73e2f88..96312dc8 100644 --- a/Panel/modules/litefm/litefm.php +++ b/Panel/modules/litefm/litefm.php @@ -24,6 +24,74 @@ require_once('includes/lib_remote.php'); +function litefm_decode_name_param($value) +{ + return rawurldecode((string)$value); +} + +function litefm_escape_html($value) +{ + return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); +} + +function litefm_is_valid_path_component($name) +{ + if (!is_string($name) || $name === '' || $name === '.' || $name === '..') { + return false; + } + if (strpos($name, "\0") !== false || strpos($name, '/') !== false || strpos($name, '\\') !== false) { + return false; + } + return true; +} + +function litefm_normalize_relative_path($relativePath) +{ + $relativePath = str_replace('\\', '/', (string)$relativePath); + $relativePath = preg_replace('#/+#', '/', $relativePath); + $relativePath = trim($relativePath, '/'); + if ($relativePath === '') { + return ''; + } + if (strpos($relativePath, "\0") !== false) { + return false; + } + if (preg_match('#(^|/)\.{1,2}(/|$)#', $relativePath)) { + return false; + } + foreach (explode('/', $relativePath) as $segment) { + if (!litefm_is_valid_path_component($segment)) { + return false; + } + } + return $relativePath; +} + +function litefm_path_within_home($homePath, $candidatePath) +{ + $homeNorm = str_replace('\\', '/', clean_path((string)$homePath)); + $candidateNorm = str_replace('\\', '/', clean_path((string)$candidatePath)); + $homeCmp = strtolower(rtrim($homeNorm, '/')); + $candidateCmp = strtolower($candidateNorm); + if ($candidateCmp === $homeCmp) { + return true; + } + return strpos($candidateCmp, $homeCmp . '/') === 0; +} + +function litefm_safe_join_home_path($homePath, $relativePath) +{ + $normalizedRel = litefm_normalize_relative_path($relativePath); + if ($normalizedRel === false) { + return false; + } + $fullPath = clean_path(rtrim((string)$homePath, '/') . '/' . $normalizedRel); + if (!litefm_path_within_home($homePath, $fullPath)) { + return false; + } + return $fullPath; +} + function do_progress($kbytes,$totalsize) { if( $totalsize != 0 ) @@ -54,7 +122,12 @@ function litefm_check($home_id) { if (isset($_GET['item']) and !isset($_GET['upload']) and !isset( $_POST['delete'] ) and !isset( $_POST['create_folder'] ) and !isset( $_POST['secureButton'] ) and !isset( $_POST['delete_check'] ) and !isset( $_POST['secure_check'] )) { - $fileName = !empty($_POST['name']) ? urldecode($_POST['name']) : urldecode($_GET['name']); + $fileName = !empty($_POST['name']) ? litefm_decode_name_param($_POST['name']) : litefm_decode_name_param(isset($_GET['name']) ? $_GET['name'] : ''); + if (!litefm_is_valid_path_component($fileName)) + { + print_failure("Path decode failed"); + return FALSE; + } if(isset($_GET['type'])){ $type = $_GET['type']; }else{ @@ -66,23 +139,29 @@ function litefm_check($home_id) $path = $_SESSION['fm_files_'.$home_id][$_GET['item']]; if($path == $fileName){ - // Make sure nobody tries to get outside thier game server by referencing the .. directory - if(preg_match("/\/\.\.\/|\||;/", $path)) - { - print_failure(get_lang("unallowed_char")); - $_SESSION['fm_cwd_'.$home_id] = NULL; - return FALSE; - } - else - { - if($type != "file"){ - $_SESSION['fm_cwd_'.$home_id] = @$_SESSION['fm_cwd_'.$home_id] . "/" . $path; - $_SESSION['fm_cwd_'.$home_id] = clean_path($_SESSION['fm_cwd_'.$home_id]); - }else{ - if((isset($_SESSION['fm_cwd_'.$home_id]) and !endsWith($_SESSION['fm_cwd_'.$home_id], $path)) or !isset($_SESSION['fm_cwd_'.$home_id])){ - $_SESSION['fm_cwd_'.$home_id] = @$_SESSION['fm_cwd_'.$home_id] . "/" . $path; - $_SESSION['fm_cwd_'.$home_id] = clean_path($_SESSION['fm_cwd_'.$home_id]); + if($type != "file"){ + $nextPath = trim((string)@$_SESSION['fm_cwd_'.$home_id], '/'); + $nextPath = $nextPath === '' ? $path : $nextPath . '/' . $path; + $normalizedNext = litefm_normalize_relative_path($nextPath); + if($normalizedNext === false) + { + print_failure(get_lang("unallowed_char")); + $_SESSION['fm_cwd_'.$home_id] = NULL; + return FALSE; + } + $_SESSION['fm_cwd_'.$home_id] = $normalizedNext; + }else{ + if((isset($_SESSION['fm_cwd_'.$home_id]) and !endsWith($_SESSION['fm_cwd_'.$home_id], $path)) or !isset($_SESSION['fm_cwd_'.$home_id])){ + $nextPath = trim((string)@$_SESSION['fm_cwd_'.$home_id], '/'); + $nextPath = $nextPath === '' ? $path : $nextPath . '/' . $path; + $normalizedNext = litefm_normalize_relative_path($nextPath); + if($normalizedNext === false) + { + print_failure(get_lang("unallowed_char")); + $_SESSION['fm_cwd_'.$home_id] = NULL; + return FALSE; } + $_SESSION['fm_cwd_'.$home_id] = $normalizedNext; } } } diff --git a/docs/modules/litefm.md b/docs/modules/litefm.md index 346eef2e..24beb4e1 100644 --- a/docs/modules/litefm.md +++ b/docs/modules/litefm.md @@ -26,12 +26,23 @@ In-panel file manager and file editor for customer server homes. - remote file listings - read/write operations +Current listing flow uses `remote_dirlistfm`, where agent filenames are treated as exact values and preserved by the Panel. + ## User Workflow - browse files - upload/download - edit common configs +Special-character names are supported end-to-end in browse/navigation/actions, including: + +- `@3739421199` +- `@CF` +- `My Mod Folder` +- `[Server] Configs` +- `profile.backup` +- `mod+keys` + ## Admin Workflow - set file management permissions @@ -43,9 +54,22 @@ In-panel file manager and file editor for customer server homes. - protected control files - shared secret exposure +Path safety model: + +- Preserve exact filename display and action values (no destructive filename stripping). +- Escape output for HTML rendering only. +- Encode filename tokens in links using `rawurlencode`; decode with `rawurldecode`. +- Reject invalid path components (`..`, path separators, NUL byte). +- Normalize relative paths and verify final resolved paths remain inside the assigned game home. +- Distinguish user-facing errors: + - `Server files have not been installed yet.` (home missing) + - `Directory not found: ` (missing subdirectory) + - `Directory exists but cannot be opened. Check permissions.` (listing/access failure) + - `Path decode failed` (invalid filename token) + ## Known Issues -- should be hardened around safe roots and backups +- very large uploads/downloads still depend on chunking/browser limits ## Missing Functionality