diff --git a/modules/servers/ArkhostVPSAG/ArkhostVPSAG.php b/modules/servers/ArkhostVPSAG/ArkhostVPSAG.php index 9a23a5e..f9c2739 100644 --- a/modules/servers/ArkhostVPSAG/ArkhostVPSAG.php +++ b/modules/servers/ArkhostVPSAG/ArkhostVPSAG.php @@ -19,8 +19,24 @@ use WHMCS\Database\Capsule; function ArkhostVPSAG_GetVPSID(array $params) { $vpsId = $params['model']->serviceProperties->get('vpsag'); - // Handle both VPSAG-xxx and VPS-xxx formats, return just the numeric ID - return str_replace(['VPSAG-', 'VPS-'], '', $vpsId); + // Handle both VPSAG-xxx and VPS-xxx formats, return just the numeric ID. + // Strip everything except digits so the value is always safe in an API path. + return preg_replace('/[^0-9]/', '', (string) $vpsId); +} + +/** + * Validate a client-supplied value before it is concatenated into an upstream + * API URL path. Rejects path traversal and any character outside the allowed + * set so a client cannot reshape the request (path/parameter injection / IDOR). + */ +function ArkhostVPSAG_SafePathSegment($value, $pattern, $label) { + $value = (string) $value; + + if ($value === '' || strpos($value, '..') !== false || !preg_match($pattern, $value)) { + throw new Exception('Invalid ' . $label); + } + + return $value; } function ArkhostVPSAG_API(array $params) { @@ -91,7 +107,8 @@ function ArkhostVPSAG_API(array $params) { break; case 'Graphs': - $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/graph/' . $params['time']; + $time = ArkhostVPSAG_SafePathSegment($params['time'], '/^[A-Za-z0-9_-]+$/', 'time period'); + $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/graph/' . $time; $method .= 'GET'; break; @@ -178,6 +195,9 @@ function ArkhostVPSAG_API(array $params) { break; case 'Reverse DNS': + if (!filter_var($params['ip'] ?? '', FILTER_VALIDATE_IP)) { + throw new Exception('Invalid IP address'); + } $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/rdns/' . $params['ip']; $method .= 'POST'; @@ -215,7 +235,8 @@ function ArkhostVPSAG_API(array $params) { break; case 'Delete backup': - $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/backup/' . $params['file']; + $file = ArkhostVPSAG_SafePathSegment($params['file'], '/^[A-Za-z0-9._-]+$/', 'backup file'); + $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/backup/' . $file; $method .= 'DELETE'; break; @@ -225,7 +246,8 @@ function ArkhostVPSAG_API(array $params) { break; case 'Restore backup': - $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/restore/' . $params['file']; + $file = ArkhostVPSAG_SafePathSegment($params['file'], '/^[A-Za-z0-9._-]+$/', 'backup file'); + $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/restore/' . $file; $method .= 'POST'; break; @@ -248,7 +270,8 @@ function ArkhostVPSAG_API(array $params) { break; case 'Delete Firewall rule': - $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/firewall/' . $params['rule_id']; + $ruleId = ArkhostVPSAG_SafePathSegment($params['rule_id'], '/^[0-9]+$/', 'rule id'); + $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/firewall/' . $ruleId; $method .= 'DELETE'; break; @@ -263,7 +286,8 @@ function ArkhostVPSAG_API(array $params) { break; case 'Load ISO': - $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/iso/' . $params['iso_id']; + $isoId = ArkhostVPSAG_SafePathSegment($params['iso_id'], '/^[0-9]+$/', 'ISO id'); + $url .= 'vps/' . ArkhostVPSAG_GetVPSID($params) . '/iso/' . $isoId; $method .= 'POST'; break; @@ -319,7 +343,17 @@ function ArkhostVPSAG_API(array $params) { } function ArkhostVPSAG_Error($func, $params, Exception $err) { - logModuleCall('ArkHost - VPSAG', $func, $params, $err->getMessage(), $err->getTraceAsString()); + // Never write the decrypted reseller API credentials into the module log. + $replaceVars = array(); + if (is_array($params)) { + foreach (array('serverpassword', 'serverusername', 'password') as $sensitive) { + if (!empty($params[$sensitive]) && is_string($params[$sensitive])) { + $replaceVars[] = $params[$sensitive]; + } + } + } + + logModuleCall('ArkHost - VPSAG', $func, $params, $err->getMessage(), $err->getTraceAsString(), $replaceVars); } function ArkhostVPSAG_MetaData() { @@ -610,7 +644,9 @@ function ArkhostVPSAG_CreateAccount(array $params) { function ArkhostVPSAG_SuspendAccount(array $params) { try { - $params['action'] = 'Stop'; + // Use Disable (not Stop) so the client cannot simply power the VPS back + // on from the client area and defeat the suspension. + $params['action'] = 'Disable'; ArkhostVPSAG_API($params); } catch (Exception $err) { ArkhostVPSAG_Error(__FUNCTION__, $params, $err); @@ -622,7 +658,8 @@ function ArkhostVPSAG_SuspendAccount(array $params) { function ArkhostVPSAG_UnsuspendAccount(array $params) { try { - $params['action'] = 'Start'; + // Mirror of SuspendAccount: re-enable the VPS that Disable suspended. + $params['action'] = 'Enable'; ArkhostVPSAG_API($params); } catch (Exception $err) { ArkhostVPSAG_Error(__FUNCTION__, $params, $err); @@ -748,6 +785,14 @@ function ArkhostVPSAG_ClientAreaAPI(array $params) { $results = array('result' => 'success'); if (in_array($action, $actions)) { + // Block state-changing actions when the service is not Active, so a + // suspended or terminated client cannot control the VPS. + $stateChanging = array('Reinstall', 'Reboot', 'Stop', 'Start', 'Hostname rDNS', 'Create backup', 'Delete backup', 'Restore backup', 'Add Firewall rules', 'Delete Firewall rule', 'Commit Firewall rules', 'Load ISO', 'Eject ISO', 'Reset root'); + $status = isset($params['model']) ? $params['model']->domainstatus : ''; + if (in_array($action, $stateChanging) && $status !== 'Active') { + throw new Exception('This action is not available while the service is not active.'); + } + foreach ($_POST as $key => $value) { $params[$key] = $value; } @@ -1036,6 +1081,9 @@ function ArkhostVPSAG_ClientArea(array $params) { 'templateVariables' => array( 'images' => $images, 'serverInfo' => $serverInfo, + // Pre-encoded for safe embedding in a . + 'serverInfoJson' => json_encode($serverInfo, JSON_HEX_TAG | JSON_HEX_AMP) ?: '{}', 'operatingSystems' => $operatingSystems, 'token' => generate_token('plain'), '_LANG' => ArkhostVPSAG_Translation(), diff --git a/modules/servers/ArkhostVPSAG/callback.php b/modules/servers/ArkhostVPSAG/callback.php index 8e11dba..1036d8e 100644 --- a/modules/servers/ArkhostVPSAG/callback.php +++ b/modules/servers/ArkhostVPSAG/callback.php @@ -18,7 +18,9 @@ require_once ROOTDIR . DIRECTORY_SEPARATOR . 'modules' . DIRECTORY_SEPARATOR . ' use WHMCS\Database\Capsule; -$_POST = array_map('html_entity_decode', $_POST); +$_POST = array_map(function ($value) { + return is_string($value) ? html_entity_decode($value) : $value; +}, $_POST); // Look for the service by VPSAG ID in service properties $serviceProperty = Capsule::table('tblservice_properties') @@ -57,7 +59,7 @@ foreach ($_POST as $key => $value) { $rawSig .= hash('sha512', decrypt($server->password)); $signature = hash('sha256', $rawSig); -if ($_POST['sig'] != $signature) { +if (!hash_equals($signature, (string) ($_POST['sig'] ?? ''))) { http_response_code(403); exit('Invalid signature'); } @@ -79,15 +81,21 @@ $_POST['ips'] = array_map(function ($ip) { $mainIP = $_POST['mainip']; -$_POST['ips'] = array_filter($_POST['ips'], function ($ip) use ($mainIP) { - return $ip !== $mainIP; -}); +// Keep every additional IPv4 (exclude the primary IP and blanks), then append +// the IPv6 address. Stored newline-separated, as WHMCS expects. +$extraIps = array_values(array_filter($_POST['ips'], function ($ip) use ($mainIP) { + return $ip !== $mainIP && $ip !== ''; +})); + +if (!empty($_POST['ipv6'])) { + $extraIps[] = $_POST['ipv6']; +} Capsule::table('tblhosting')->where('id', $service->id)->update(array( 'username' => $_POST['username'], 'password' => encrypt($_POST['root']), 'dedicatedip' => $mainIP, - 'assignedips' => (!array_shift($_POST['ips']) ? $_POST['ipv6'] : implode('\n', $_POST['ips']) . '\n' . $_POST['ipv6']), + 'assignedips' => implode("\n", $extraIps), )); echo '*ok*'; diff --git a/modules/servers/ArkhostVPSAG/template/clientarea_direct.tpl b/modules/servers/ArkhostVPSAG/template/clientarea_direct.tpl index 6435e31..29b26bb 100644 --- a/modules/servers/ArkhostVPSAG/template/clientarea_direct.tpl +++ b/modules/servers/ArkhostVPSAG/template/clientarea_direct.tpl @@ -651,7 +651,7 @@ if (arkhostVNCHashDetected) {