diff --git a/include/admin/Tools/Status.php b/include/admin/Tools/Status.php index cfa4307..6c5a78f 100644 --- a/include/admin/Tools/Status.php +++ b/include/admin/Tools/Status.php @@ -4,51 +4,51 @@ namespace gp\admin\Tools; defined('is_running') or die('Not an entry point...'); -class Status extends \gp\special\Base{ +class Status extends \gp\special\Base { - protected $check_dir_len = 0; - protected $failed_count = 0; - protected $failed = []; - protected $passed_count = 0; - protected $show_failed_max = 50; - protected $deletable = []; + protected $check_dir_len = 0; + protected $failed_count = 0; + protected $failed = []; + protected $passed_count = 0; + protected $show_failed_max = 50; + protected $deletable = []; protected $euid; - public function __construct(){ + public function __construct() { } - public function RunScript(){ + public function RunScript() { global $langmessage; - echo '

'.$langmessage['Site Status'].'

'; + echo '

' . $langmessage['Site Status'] . '

'; $cmd = \gp\tool::GetCommand(); - switch($cmd){ + switch ($cmd) { case 'FixOwner': $this->FixOwner(); - break; + break; } $this->CheckDataDir(); $this->DefaultDisplay(); } - public function CheckDataDir(){ + public function CheckDataDir() { global $dataDir; - $this->check_dir_len = 0; - $this->failed_count = 0; - $this->passed_count = 0; - $this->show_failed_max = 50; + $this->check_dir_len = 0; + $this->failed_count = 0; + $this->passed_count = 0; + $this->show_failed_max = 50; - $check_dir = $dataDir.'/data'; - $this->check_dir_len = strlen($check_dir); - $this->euid = '?'; + $check_dir = $dataDir . '/data'; + $this->check_dir_len = strlen($check_dir); + $this->euid = '?'; - if( function_exists('posix_geteuid') ){ + if (function_exists('posix_geteuid')) { $this->euid = posix_geteuid(); } @@ -56,104 +56,107 @@ class Status extends \gp\special\Base{ $this->CheckDir($check_dir); } - public function DefaultDisplay(){ - global $langmessage, $dataDir; + public function DefaultDisplay() { + global $langmessage, $dataDir; - $check_dir = $dataDir.'/data'; - $checked = $this->passed_count + $this->failed_count; + $check_dir = $dataDir . '/data'; + $checked = $this->passed_count + $this->failed_count; - if( $this->failed_count === 0 ){ - echo '

'; - echo sprintf($langmessage['data_check_passed'],$checked,$checked); - echo '

'; - $this->ShowDeletable(); - - return; - } - - echo '

'; - echo sprintf($langmessage['data_check_failed'],$this->failed_count,$checked); + if ($this->failed_count === 0) { + echo '

'; + echo sprintf($langmessage['data_check_passed'], $checked, $checked); echo '

'; - - - // the /data directory isn't writable - if( count($this->failed) == 1 && in_array($check_dir,$this->failed) ){ - echo '

'; - echo 'WARNING: Your data directory at is no longer writable: '.$check_dir; - echo '

'; - return; - } - - - if( $this->failed_count > $this->show_failed_max ){ - echo '

'; - echo sprintf($langmessage['showing_max_failed'],$this->show_failed_max); - echo '

'; - } - - - echo ''; - echo ''; - - - // sort by strlen to get directories first - usort($this->failed, function($a, $b) { - return strlen($a) - strlen($b); - }); - - foreach($this->failed as $i => $path){ - - if( $i > $this->show_failed_max ){ - break; - } - - $readable_path = substr($path,$this->check_dir_len); - $euid = \gp\install\FilePermissions::file_uid($path); - - echo ''; - } - - echo '
'; - echo $langmessage['file_name']; - echo ''; - echo $langmessage['File Owner']; - echo '
'; - echo $langmessage['Current_Value']; - echo '
'; - echo '
'; - echo $langmessage['Expected_Value']; - echo '
 '; - echo '
'; - echo $readable_path; - echo ''; - - echo $this->ShowUser($euid); - echo ''; - echo $this->ShowUser($this->euid); - echo ''; - echo \gp\tool::Link('Admin/Status','Fix','cmd=FixOwner&path='.rawurlencode($readable_path),'data-cmd="cnreq"'); - echo '
'; - - $this->CheckPageFiles(); $this->ShowDeletable(); + return; } + echo '

'; + echo sprintf($langmessage['data_check_failed'], $this->failed_count, $checked); + echo '

'; + + + // the /data directory isn't writable + if (count($this->failed) == 1 && in_array($check_dir, $this->failed)) { + echo '

'; + echo 'WARNING: Your data directory at is no longer writable: ' . htmlspecialchars($check_dir, ENT_QUOTES, 'UTF-8'); // escape output + echo '

'; + return; + } + + + if ($this->failed_count > $this->show_failed_max) { + echo '

'; + echo sprintf($langmessage['showing_max_failed'], $this->show_failed_max); + echo '

'; + } + + + echo ''; + echo ''; + + // sort by strlen to get directories first + usort($this->failed, function ($a, $b) { + return strlen($a) - strlen($b); + }); + + foreach ($this->failed as $i => $path) { + + if ($i > $this->show_failed_max) { + break; + } + + $readable_path = substr($path, $this->check_dir_len); + + // Sanitize the readable_path BEFORE passing it to rawurlencode and \gp\tool::Link + $safe_readable_path = $this->sanitizePath($readable_path); + + $euid = \gp\install\FilePermissions::file_uid($path); + + echo ''; + } + + echo '
'; + echo $langmessage['file_name']; + echo ''; + echo $langmessage['File Owner']; + echo '
'; + echo $langmessage['Current_Value']; + echo '
'; + echo '
'; + echo $langmessage['Expected_Value']; + echo '
'; + echo '
'; + echo htmlspecialchars($readable_path, ENT_QUOTES, 'UTF-8'); // escape output + echo ''; + + echo $this->ShowUser($euid); + echo ''; + echo $this->ShowUser($this->euid); + echo ''; + // Use the sanitized path + echo \gp\tool::Link('Admin/Status', 'Fix', 'cmd=FixOwner&path=' . rawurlencode($safe_readable_path), 'data-cmd="cnreq"'); + echo '
'; + + $this->CheckPageFiles(); + $this->ShowDeletable(); +} + + /** * Show Deletable Files */ - protected function ShowDeletable(){ - if( empty($this->deletable) ){ + protected function ShowDeletable() { + if (empty($this->deletable)) { return; } echo '

Deletable Files

'; echo '
    '; - foreach($this->deletable as $file){ - echo '
  1. '.htmlspecialchars($file).'
  2. '; + foreach ($this->deletable as $file) { + echo '
  3. ' . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '
  4. '; // escape output } echo '
'; - } @@ -162,32 +165,32 @@ class Status extends \gp\special\Base{ * Check page files for orphaned data files * */ - protected function CheckPageFiles(){ - global $dataDir,$gp_index; + protected function CheckPageFiles() { + global $dataDir, $gp_index; - $pages_dir = $dataDir.'/data/_pages'; - $all_files = \gp\tool\Files::ReadDir($pages_dir,'php'); - foreach($all_files as $key => $file){ - $all_files[$key] = $pages_dir.'/'.$file.'.php'; + $pages_dir = $dataDir . '/data/_pages'; + $all_files = \gp\tool\Files::ReadDir($pages_dir, 'php'); + foreach ($all_files as $key => $file) { + $all_files[$key] = $pages_dir . '/' . $file . '.php'; } $page_files = array(); - foreach($gp_index as $slug => $index){ + foreach ($gp_index as $slug => $index) { $page_files[] = \gp\tool\Files::PageFile($slug); } - $diff = array_diff($all_files,$page_files); + $diff = array_diff($all_files, $page_files); - if( !count($diff) ){ + if (!count($diff)) { return; } echo '

Orphaned Data Files

'; echo '

The following data files appear to be orphaned and are most likely no longer needed. Before completely removing these files, we recommend backing them up first.

'; echo ''; - foreach($diff as $file){ + foreach ($diff as $file) { echo ''; } echo '
File
' - . $file + . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') // escape output . '
'; @@ -198,59 +201,58 @@ class Status extends \gp\special\Base{ * @param string $dir * */ - protected function CheckDir($dir){ + protected function CheckDir($dir) { - if( !$this->CheckFile($dir) ){ + if (!$this->CheckFile($dir)) { return; } $dh = @opendir($dir); - if( $dh === false ){ + if ($dh === false) { $this->failed_count++; $this->failed[] = $dir; return; } - while( ($file = readdir($dh)) !== false){ - if( $file === '.' || $file === '..' ){ + while (($file = readdir($dh)) !== false) { + if ($file === '.' || $file === '..') { continue; } - $full_path = $dir.'/'.$file; - if( is_link($full_path) ){ + $full_path = $dir . '/' . $file; + if (is_link($full_path)) { continue; } - if( preg_match('#x-deletable-[0-9]+#',$full_path) ){ + if (preg_match('#x-deletable-[0-9]+#', $full_path)) { $this->deletable[] = $full_path; continue; } - if( is_dir($full_path) ){ + if (is_dir($full_path)) { $this->CheckDir($full_path); - }else{ - $this->CheckFile($full_path,'file'); + } else { + $this->CheckFile($full_path, 'file'); } } } - protected function CheckFile($path,$type='dir'){ + protected function CheckFile($path, $type = 'dir') { - if( \gp\install\FilePermissions::HasFunctions() ){ - $current = @substr(decoct( @fileperms($path)), -3); + if (\gp\install\FilePermissions::HasFunctions()) { + $current = @substr(decoct(@fileperms($path)), -3); - if( $type === 'file' ){ + if ($type === 'file') { $expected = \gp\install\FilePermissions::getExpectedPerms_file($path); - }else{ + } else { $expected = \gp\install\FilePermissions::getExpectedPerms($path); } - if( \gp\install\FilePermissions::perm_compare($expected,$current) ){ + if (\gp\install\FilePermissions::perm_compare($expected, $current)) { $this->passed_count++; return true; } - - }elseif( gp_is_writable($path) ){ + } elseif (gp_is_writable($path)) { $this->passed_count++; return true; } @@ -266,10 +268,10 @@ class Status extends \gp\special\Base{ * Display a user name and uid * @param int $uid */ - protected function ShowUser($uid){ + protected function ShowUser($uid) { $user_info = posix_getpwuid($uid); - if( $user_info ){ - return $user_info['name'].' ('.$uid.')'; + if ($user_info) { + return htmlspecialchars($user_info['name'], ENT_QUOTES, 'UTF-8') . ' (' . $uid . ')'; // escape output } return $uid; @@ -284,65 +286,112 @@ class Status extends \gp\special\Base{ * 4) attempt to delete temp folder * */ - public function FixOwner(){ - global $dataDir, $langmessage; + + + public function FixOwner() { + global $dataDir, $langmessage; + // Sanitize the path from user input + $unsafe_path = isset($_REQUEST['path']) ? $_REQUEST['path'] : ''; + $safe_path = $this->sanitizePath($unsafe_path); - $to_fix = '/data'.$_REQUEST['path']; - $to_fix_full = $dataDir . $to_fix; - $new_file = \gp\tool\FileSystem::TempFile($to_fix); - $new_file_full = $dataDir . $new_file; - $deletable = \gp\tool\FileSystem::TempFile(dirname($to_fix).'/x-deletable'); - $deletable_full = $dataDir . $deletable; - - if( !\gp\tool\Files::CheckPath( $to_fix_full ) ){ - msg($langmessage['OOPS'].' Invalid Path'); - return; - } - - - echo '
    '; - echo '
  1. Copy: '.$to_fix.' -> ' . $new_file . '
  2. '; - - if( !\gp\admin\Tools\Port::CopyAll($to_fix_full,$new_file_full) ){ - echo '
  3. Failed
  4. '; - echo '
'; - msg($langmessage['OOPS'].' Not Copied'); - \gp\tool\Files::RmAll($new_file_full); - return; - } - - - // move old to deletable - echo '
  • Move: '.htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8').' -> ' . $deletable . '
  • '; - if( !rename($to_fix_full,$deletable_full) ){ - echo '
  • Failed
  • '; - echo ''; - msg($langmessage['OOPS'].' Rename to deletable failed'); - \gp\tool\Files::RmAll($new_file_full); - return; - } - - - // move - echo '
  • Move: '.$new_file.' -> ' . $to_fix . '
  • '; - if( !rename($new_file_full, $to_fix_full) ){ - echo '
  • Failed
  • '; - echo ''; - msg($langmessage['OOPS'].' Rename to old failed'); - return; - } - - echo '
  • Success
  • '; - - // attempt to remove deletable - if( !\gp\tool\Files::RmAll($deletable_full) ){ - echo '
  • Note: '.$deletable.' was not deleted
  • '; - } - - echo ''; - + // Validate path AFTER sanitization + if (strpos($safe_path, 'javascript:') !== false) { + msg($langmessage['OOPS'] . ' Invalid Path: javascript: URI is not allowed.'); + return; + } + if (strpos($safe_path, 'data:') !== false) { + msg($langmessage['OOPS'] . ' Invalid Path: data: URI is not allowed.'); + return; } + $to_fix = '/data' . $safe_path; + $to_fix_full = $dataDir . $to_fix; + $new_file = \gp\tool\FileSystem::TempFile($to_fix); + $new_file_full = $dataDir . $new_file; + + $deletable = \gp\tool\FileSystem::TempFile(dirname($to_fix) . '/x-deletable'); + $deletable_full = $dataDir . $deletable; + + if (!\gp\tool\Files::CheckPath($to_fix_full)) { + msg($langmessage['OOPS'] . ' Invalid Path'); + return; + } + + echo '
      '; + echo '
    1. Copy: ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($new_file, ENT_QUOTES, 'UTF-8') . '
    2. '; + + if (!\gp\admin\Tools\Port::CopyAll($to_fix_full, $new_file_full)) { + echo '
    3. Failed
    4. '; + echo '
    '; + msg($langmessage['OOPS'] . ' Not Copied'); + \gp\tool\Files::RmAll($new_file_full); + return; + } + + // move old to deletable + echo '
  • Move: ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($deletable, ENT_QUOTES, 'UTF-8') . '
  • '; + if (!rename($to_fix_full, $deletable_full)) { + echo '
  • Failed
  • '; + echo ''; + msg($langmessage['OOPS'] . ' Rename to deletable failed'); + \gp\tool\Files::RmAll($new_file_full); + return; + } + + // move + echo '
  • Move: ' . htmlspecialchars($new_file, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . '
  • '; + if (!rename($new_file_full, $to_fix_full)) { + echo '
  • Failed
  • '; + echo ''; + msg($langmessage['OOPS'] . ' Rename to old failed'); + return; + } + + echo '
  • Success
  • '; + + // attempt to remove deletable + if (!\gp\tool\Files::RmAll($deletable_full)) { + echo '
  • Note: ' . htmlspecialchars($deletable, ENT_QUOTES, 'UTF-8') . ' was not deleted
  • '; + } + + echo ''; } + /** + * Sanitizes a path to prevent directory traversal and XSS. + * + * @param string $path The path to sanitize. + * @return string The sanitized path. + */ + private function sanitizePath(string $path): string +{ + // Block Javascript and data URI + if (stripos($path, 'javascript:') !== false || stripos($path, 'data:') !== false) { + return ''; // or throw an exception, or return a safe default path + } + + // Remove any characters that aren't alphanumeric, underscores, dashes, or slashes + $sanitized = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', $path); + + // Resolve directory traversal attempts (e.g., ../../) + $sanitized = str_replace('..', '', $sanitized); + + // Remove potentially harmful URL components + $sanitized = str_replace('href', '', strtolower($sanitized)); + + // Remove leading and trailing slashes + $sanitized = trim($sanitized, '/'); + + // Ensure no double slashes + $sanitized = preg_replace('/\/+/', '/', $sanitized); + + // Add a leading slash + if ($sanitized !== '') { + $sanitized = '/' . $sanitized; + } + + return $sanitized; + } + +} \ No newline at end of file