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) {