diff --git a/interface/lib/classes/functions.inc.php b/interface/lib/classes/functions.inc.php index 03e331f0f14c22db8632e191e9e6a5346401b693..4d4c011fb5e5e4b06bed617601bd7bac42f345d8 100644 --- a/interface/lib/classes/functions.inc.php +++ b/interface/lib/classes/functions.inc.php @@ -61,7 +61,7 @@ class functions { if(is_string($to) && strpos($to, ',') !== false) { $to = preg_split('/\s*,\s*/', $to); } - + $app->ispcmail->send($to); $app->ispcmail->finish(); @@ -234,7 +234,7 @@ class functions { if(preg_match($regex, $result['ip'])) $ips[] = $result['ip']; } } - + $results = $app->db->queryAllRecords("SELECT remote_ips FROM web_database WHERE remote_ips != ''"); if(!empty($results) && is_array($results)){ foreach($results as $result){ @@ -290,6 +290,34 @@ class functions { return round(pow(1024, $base-floor($base)), $precision).$suffixes[floor($base)]; } + + /** + * Normalize a path and strip duplicate slashes from it + * + * This will also remove all /../ from the path, reducing the preceding path elements + * + * @param string $path + * @return string + */ + public function normalize_path($path) { + $path = preg_replace('~[/]{2,}~', '/', $path); + $parts = explode('/', $path); + $return_parts = array(); + + foreach($parts as $current_part) { + if($current_part === '..') { + if(!empty($return_parts) && end($return_parts) !== '') { + array_pop($return_parts); + } + } else { + $return_parts[] = $current_part; + } + } + + return implode('/', $return_parts); + } + + /** IDN converter wrapper. * all converter classes should be placed in ISPC_CLASS_PATH.'/idn/' */ @@ -370,42 +398,42 @@ class functions { public function is_allowed_user($username, $restrict_names = false) { global $app; - + $name_blacklist = array('root','ispconfig','vmail','getmail'); if(in_array($username,$name_blacklist)) return false; - + if(preg_match('/^[a-zA-Z0-9\.\-_]{1,32}$/', $username) == false) return false; - + if($restrict_names == true && preg_match('/^web\d+$/', $username) == false) return false; - + return true; } - + public function is_allowed_group($groupname, $restrict_names = false) { global $app; - + $name_blacklist = array('root','ispconfig','vmail','getmail'); if(in_array($groupname,$name_blacklist)) return false; - + if(preg_match('/^[a-zA-Z0-9\.\-_]{1,32}$/', $groupname) == false) return false; - + if($restrict_names == true && preg_match('/^client\d+$/', $groupname) == false) return false; - + return true; } - + public function getimagesizefromstring($string){ if (!function_exists('getimagesizefromstring')) { $uri = 'data://application/octet-stream;base64,' . base64_encode($string); return getimagesize($uri); } else { return getimagesizefromstring($string); - } + } } - + public function password($minLength = 10, $special = false){ global $app; - + $iteration = 0; $password = ""; $maxLength = $minLength + 5; @@ -430,7 +458,7 @@ class functions { public function getRandomInt($min, $max){ return floor((mt_rand() / mt_getrandmax()) * ($max - $min + 1)) + $min; } - + public function generate_customer_no(){ global $app; // generate customer no. @@ -438,13 +466,13 @@ class functions { while($app->db->queryOneRecord("SELECT client_id FROM client WHERE customer_no = ?", $customer_no)) { $customer_no = mt_rand(100000, 999999); } - + return $customer_no; } - + public function generate_ssh_key($client_id, $username = ''){ global $app; - + // generate the SSH key pair for the client $id_rsa_file = '/tmp/'.uniqid('',true); $id_rsa_pub_file = $id_rsa_file.'.pub'; @@ -458,7 +486,7 @@ class functions { $app->log("Failed to create SSH keypair for ".$username, LOGLEVEL_WARN); } } - + public function htmlentities($value) { global $conf; @@ -474,10 +502,10 @@ class functions { } else { $out = htmlentities($value, ENT_QUOTES, $conf["html_content_encoding"]); } - + return $out; } - + // Function to check paths before we use it as include. Use with absolute paths only. public function check_include_path($path) { if(strpos($path,'//') !== false) die('Include path seems to be an URL: '.$this->htmlentities($path)); @@ -488,7 +516,7 @@ class functions { if(substr($path,0,strlen(ISPC_ROOT_PATH)) != ISPC_ROOT_PATH) die('Path '.$this->htmlentities($path).' is outside of ISPConfig installation directory.'); return $path; } - + // Function to check language strings public function check_language($language) { global $app; @@ -496,10 +524,10 @@ class functions { return $language; } else { $app->log('Wrong language string: '.$this->htmlentities($language),1); - return 'en'; + return 'en'; } } - + } ?> diff --git a/interface/lib/classes/tform_base.inc.php b/interface/lib/classes/tform_base.inc.php index 91a855872c9600939ac338f2b5cbe5bd11513d73..72ddb4b6ae3e3633aeb3fdbf93e90ffbb90a07dd 100644 --- a/interface/lib/classes/tform_base.inc.php +++ b/interface/lib/classes/tform_base.inc.php @@ -399,7 +399,7 @@ class tform_base { $tmp_key = $limit_parts[2]; $allowed = $allowed = explode(',',$tmp_conf[$tmp_key]); } - + if($formtype == 'CHECKBOX') { if(strstr($limit,'force_')) { // Force the checkbox field to be ticked and enabled @@ -958,6 +958,9 @@ class tform_base { case 'STRIPNL': $returnval = str_replace(array("\n","\r"),'', $returnval); break; + case 'NORMALIZEPATH': + $returnval = $app->functions->normalize_path($returnval); + break; default: $this->errorMessage .= "Unknown Filter: ".$filter['type']; break; diff --git a/interface/web/sites/form/shell_user.tform.php b/interface/web/sites/form/shell_user.tform.php index f4e83a1b57e9b7469142286dab4cfb4a5ce7732a..523a03687aebee794ac6b91b915483e07ff02c21 100644 --- a/interface/web/sites/form/shell_user.tform.php +++ b/interface/web/sites/form/shell_user.tform.php @@ -232,6 +232,12 @@ if($_SESSION["s"]["user"]["typ"] == 'admin') { 'dir' => array ( 'datatype' => 'VARCHAR', 'formtype' => 'TEXT', + 'filters' => array( + 0 => array ( + 'event' => 'SAVE', + 'type' => 'NORMALIZEPATH' + ) + ), 'validators' => array ( 0 => array ( 'type' => 'NOTEMPTY', 'errmsg'=> 'directory_error_empty'), 1 => array ( 'type' => 'REGEX', diff --git a/server/lib/classes/functions.inc.php b/server/lib/classes/functions.inc.php index 5da1f3d713029b069b3a89c954dc001a541e1271..5296c3012b65cb4bf0d9889c893252e99ec9d4a8 100644 --- a/server/lib/classes/functions.inc.php +++ b/server/lib/classes/functions.inc.php @@ -356,6 +356,34 @@ class functions { } } + + /** + * Normalize a path and strip duplicate slashes from it + * + * This will also remove all /../ from the path, reducing the preceding path elements + * + * @param string $path + * @return string + */ + public function normalize_path($path) { + $path = preg_replace('~[/]{2,}~', '/', $path); + $parts = explode('/', $path); + $return_parts = array(); + + foreach($parts as $current_part) { + if($current_part === '..') { + if(!empty($return_parts) && end($return_parts) !== '') { + array_pop($return_parts); + } + } else { + $return_parts[] = $current_part; + } + } + + return implode('/', $return_parts); + } + + /** IDN converter wrapper. * all converter classes should be placed in ISPC_CLASS_PATH.'/idn/' */ @@ -435,10 +463,10 @@ class functions { } return implode("\n", $domains); } - + public function generate_ssh_key($client_id, $username = ''){ global $app; - + // generate the SSH key pair for the client $id_rsa_file = '/tmp/'.uniqid('',true); $id_rsa_pub_file = $id_rsa_file.'.pub'; diff --git a/server/lib/classes/system.inc.php b/server/lib/classes/system.inc.php index 131d10f2442f2f5115a757ab0e49a249adb270d5..a26707b0aea1a76258773b8f8902e34b66112d76 100644 --- a/server/lib/classes/system.inc.php +++ b/server/lib/classes/system.inc.php @@ -2300,6 +2300,36 @@ class system{ return true; } + public function is_allowed_path($path) { + global $app; + + $path = $app->functions->normalize_path($path); + if(file_exists($path)) { + $path = realpath($path); + } + + $blacklisted_paths_regex = array( + '@^/$@', + '@^/proc(/.*)?$@', + '@^/sys(/.*)?$@', + '@^/etc(/.*)?$@', + '@^/dev(/.*)?$@', + '@^/tmp(/.*)?$@', + '@^/run(/.*)?$@', + '@^/boot(/.*)?$@', + '@^/root(/.*)?$@', + '@^/var(/?|/backups?(/.*)?)?$@', + ); + + foreach($blacklisted_paths_regex as $regex) { + if(preg_match($regex, $path)) { + return false; + } + } + + return true; + } + public function last_exec_out() { return $this->_last_exec_out; } @@ -2350,6 +2380,8 @@ class system{ } public function create_jailkit_user($username, $home_dir, $user_home_dir, $shell = '/bin/bash', $p_user = null, $p_user_home_dir = null) { + global $app; + // Disallow operating on root directory if(realpath($home_dir) == '/') { $app->log("create_jailkit_user: invalid home_dir: $home_dir", LOGLEVEL_WARN); @@ -2379,6 +2411,8 @@ class system{ } public function create_jailkit_chroot($home_dir, $app_sections = array(), $options = array()) { + global $app; + // Disallow operating on root directory if(realpath($home_dir) == '/') { $app->log("create_jailkit_chroot: invalid home_dir: $home_dir", LOGLEVEL_WARN); @@ -2450,6 +2484,8 @@ class system{ } public function create_jailkit_programs($home_dir, $programs = array(), $options = array()) { + global $app; + // Disallow operating on root directory if(realpath($home_dir) == '/') { $app->log("create_jailkit_programs: invalid home_dir: $home_dir", LOGLEVEL_WARN); diff --git a/server/plugins-available/shelluser_base_plugin.inc.php b/server/plugins-available/shelluser_base_plugin.inc.php index 71653cf5c2d6fc9e36dcf914eefc02365ff3336b..f9a316d90e195e6e35a68eceba8b67744c08a837 100755 --- a/server/plugins-available/shelluser_base_plugin.inc.php +++ b/server/plugins-available/shelluser_base_plugin.inc.php @@ -96,6 +96,14 @@ class shelluser_base_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); + return false; + } + if($data['new']['active'] != 'y' || $data['new']['chroot'] == "jailkit") $data['new']['shell'] = '/bin/false'; if($app->system->is_user($data['new']['puser'])) { @@ -207,6 +215,14 @@ class shelluser_base_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); + return false; + } + if($data['new']['active'] != 'y') $data['new']['shell'] = '/bin/false'; if($app->system->is_user($data['new']['puser'])) { @@ -304,6 +320,14 @@ class shelluser_base_plugin { return false; } + if(is_file($data['old']['dir']) || is_link($data['old']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['old']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['old']['dir'], LOGLEVEL_WARN); + return false; + } + if($app->system->is_user($data['old']['username'])) { // Get the UID of the user $userid = intval($app->system->getuid($data['old']['username'])); diff --git a/server/plugins-available/shelluser_jailkit_plugin.inc.php b/server/plugins-available/shelluser_jailkit_plugin.inc.php index 3f8d94d2a7787251f355f60fe41989db49f84e24..dbc3d8041b22b72180cf49a9eb9a931f7454796c 100755 --- a/server/plugins-available/shelluser_jailkit_plugin.inc.php +++ b/server/plugins-available/shelluser_jailkit_plugin.inc.php @@ -89,6 +89,15 @@ class shelluser_jailkit_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); + return false; + } + + if($app->system->is_user($data['new']['puser'])) { // Get the UID of the parent user $uid = intval($app->system->getuid($data['new']['puser'])); @@ -170,6 +179,14 @@ class shelluser_jailkit_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); + return false; + } + if($app->system->is_user($data['new']['puser'])) { $web = $app->db->queryOneRecord("SELECT * FROM web_domain WHERE domain_id = ?", $data['new']['parent_domain_id']); @@ -241,6 +258,14 @@ class shelluser_jailkit_plugin { return false; } + if(is_file($data['old']['dir']) || is_link($data['old']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['old']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['old']['dir'], LOGLEVEL_WARN); + return false; + } + if ($data['old']['chroot'] == "jailkit") { $web = $app->db->queryOneRecord("SELECT * FROM web_domain WHERE domain_id = ?", $data['old']['parent_domain_id']); @@ -518,6 +543,9 @@ class shelluser_jailkit_plugin { } //* Get the keys $existing_keys = file($sshkeys, FILE_IGNORE_NEW_LINES); + if(!$existing_keys) { + $existing_keys = array(); + } $new_keys = explode("\n", $sshrsa); $old_keys = explode("\n", $this->data['old']['ssh_rsa']);