Harden VPSAG module: credential logging, path injection, suspension, signature, XSS

- Redact decrypted reseller API credentials from module logs (ArkhostVPSAG_Error).
- Validate client-controlled values before they enter upstream API URL paths
  (graph time, backup file, firewall rule id, ISO id, rDNS IP); restrict VPS ID
  to digits. Prevents path/parameter injection (IDOR).
- Suspend via Disable/Enable instead of Stop/Start, and reject state-changing
  client-area actions on non-active services (suspended client can't power on).
- Verify the provisioning callback signature with hash_equals instead of !=.
- Fix callback assigned-IP assembly: keep all extra IPv4s, join with real
  newlines (was dropping the first IP and using a literal backslash-n).
- Make callback html_entity_decode array-safe.
- Escape untrusted output: JSON_HEX_TAG-encode serverInfo for the <script>
  block, HTML-escape serverInfo/OS fields in template + error.tpl, escape
  firewall/backup values built via innerHTML.
This commit is contained in:
Yuri Karamian
2026-06-22 00:53:52 +02:00
parent 00a1921c77
commit 7ccc92a039
4 changed files with 104 additions and 38 deletions
@@ -651,7 +651,7 @@ if (arkhostVNCHashDetected) {
<script type="text/javascript">
var productURL = '{$WEB_ROOT}/clientarea.php?action=productdetails&id={$serviceid}';
var serverInfoInitial = JSON.parse('{$serverInfo|json_encode}');
var serverInfoInitial = {$serverInfoJson nofilter};
var lang = {
moduleactionfailed: 'Action failed!',
moduleactionsuccess: 'Action completed successfully',
@@ -907,6 +907,16 @@ if (arkhostVNCHashDetected) {
return false;
}
// Escape untrusted values before inserting them into innerHTML.
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function updateBackupTable(data) {
var tableBody = document.querySelector('#backupTable tbody');
tableBody.innerHTML = '';
@@ -956,9 +966,9 @@ if (arkhostVNCHashDetected) {
statusBadge = '<span class="badge badge-danger">' + lang.backups.error + '</span>';
}
row.innerHTML =
'<td>' + backupDate + '</td>' +
'<td>' + (backup.size || 'N/A') + '</td>' +
row.innerHTML =
'<td>' + escapeHtml(backupDate) + '</td>' +
'<td>' + escapeHtml(backup.size || 'N/A') + '</td>' +
'<td>' + backupType + '</td>' +
'<td>' + statusBadge + '</td>' +
'<td style="white-space: nowrap;">' +
@@ -1075,12 +1085,12 @@ if (arkhostVNCHashDetected) {
rulesList.forEach(function(rule) {
if (rule && rule.id) {
var row = document.createElement('tr');
row.innerHTML =
'<td>' + (rule.action || 'N/A') + '</td>' +
'<td>' + (rule.port || 'N/A') + '</td>' +
'<td>' + (rule.protocol || 'N/A') + '</td>' +
'<td>' + (rule.source || 'N/A') + '</td>' +
'<td>' + (rule.note || 'N/A') + '</td>' +
row.innerHTML =
'<td>' + escapeHtml(rule.action || 'N/A') + '</td>' +
'<td>' + escapeHtml(rule.port || 'N/A') + '</td>' +
'<td>' + escapeHtml(rule.protocol || 'N/A') + '</td>' +
'<td>' + escapeHtml(rule.source || 'N/A') + '</td>' +
'<td>' + escapeHtml(rule.note || 'N/A') + '</td>' +
'<td>' +
'<a href="#" onclick="ArkHostVPS_API(\'Delete Firewall rule\', true, { rule_id: \'' + rule.id + '\' }); return false;" title="Delete">' +
'<i class="fa fa-trash text-danger"></i>' +
@@ -1575,16 +1585,16 @@ if (arkhostVNCHashDetected) {
<table class="table table-sm table-borderless mb-0">
<tr>
<td class="text-muted" width="40%">{$_LANG['Overview']['Hostname']}:</td>
<td><strong>{$serverInfo['hostname']|default:'N/A'}</strong></td>
<td><strong>{$serverInfo['hostname']|default:'N/A'|escape}</strong></td>
</tr>
<tr>
<td class="text-muted">{$_LANG['IPv4']} Address:</td>
<td><code>{$serverInfo['ip']}</code></td>
<td><code>{$serverInfo['ip']|escape}</code></td>
</tr>
{if $serverInfo['ipv6']}
<tr>
<td class="text-muted">{$_LANG['IPv6']} Address:</td>
<td><code style="font-size: 11px;">{$serverInfo['ipv6']}</code></td>
<td><code style="font-size: 11px;">{$serverInfo['ipv6']|escape}</code></td>
</tr>
{/if}
<tr>
@@ -1593,7 +1603,7 @@ if (arkhostVNCHashDetected) {
{if $serverInfo['operatingSystem']['image']}
<img src="{$serverInfo['operatingSystem']['image']}" width="20" height="20" class="mr-1" style="vertical-align: middle;">
{/if}
{$serverInfo['operatingSystem']['name']|default:'N/A'}
{$serverInfo['operatingSystem']['name']|default:'N/A'|escape}
</td>
</tr>
</table>
@@ -1605,17 +1615,17 @@ if (arkhostVNCHashDetected) {
<td>
<img src="{$serverInfo['statusImage']}" height="20" class="mr-1" style="vertical-align: middle;">
<span class="{if $serverInfo['status'] == 'running'}text-success{else}text-danger{/if}">
<strong>{$serverInfo['statusDescription']}</strong>
<strong>{$serverInfo['statusDescription']|escape}</strong>
</span>
</td>
</tr>
<tr>
<td class="text-muted">{$_LANG['Overview']['Uptime']}:</td>
<td><strong>{$serverInfo['uptime_text']|default:'N/A'}</strong></td>
<td><strong>{$serverInfo['uptime_text']|default:'N/A'|escape}</strong></td>
</tr>
<tr>
<td class="text-muted">{$_LANG['Overview']['ServerType']}:</td>
<td><strong>{$serverInfo['server_type']|default:'Standard'}</strong></td>
<td><strong>{$serverInfo['server_type']|default:'Standard'|escape}</strong></td>
</tr>
</table>
</div>
@@ -1993,7 +2003,7 @@ if (arkhostVNCHashDetected) {
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{$_LANG['Settings']['Hostname']['Title']}:</label>
<input class="form-control" id="hostnameRDNS" type="text" size="30" maxlength="128" value="{$serverInfo['hostname']}">
<input class="form-control" id="hostnameRDNS" type="text" size="30" maxlength="128" value="{$serverInfo['hostname']|escape}">
</div>
</div>
</div>
@@ -2045,7 +2055,7 @@ if (arkhostVNCHashDetected) {
<label class="form-label">{$_LANG['Settings']['Password']['Title']}:</label>
<div class="input-group mb-3">
<input class="form-control" id="vpsPassword" type="password" size="30" maxlength="128" disabled value="{if $serverInfo['install_root'] != ''}{$serverInfo['install_root']}{else}Expired{/if}">
<input class="form-control" id="vpsPassword" type="password" size="30" maxlength="128" disabled value="{if $serverInfo['install_root'] != ''}{$serverInfo['install_root']|escape}{else}Expired{/if}">
<button class="btn btn-outline-secondary" type="button" onclick="ArkHostVPS_ShowPassword();return false;">
<i class="fa fa-eye" id="showPasswordIcon" aria-hidden="true"></i>
</button>
@@ -2080,14 +2090,14 @@ if (arkhostVNCHashDetected) {
</div>
<div class="media-body float-left text-left p-2">
<h6 class="distro_name media-heading">{$operatingSystemsGroup['name']}</h6>
<h6 class="distro_name media-heading">{$operatingSystemsGroup['name']|escape}</h6>
<span id="{$group}-version" class="version small">{$_LANG['Settings']['Reinstall']['Version']}</span>
</div>
</button>
<div class="os_badge_list dropdown-menu w-100">
{foreach from=$operatingSystemsGroup['versions'] item=$operatingSystem}
<a class="dropdown-item" href="#" data-os="{$operatingSystem['id']}" data-group="{$group}" onclick="ArkHostVPS_ChooseOS(this);return false;">{$operatingSystem['name']}</a>
<a class="dropdown-item" href="#" data-os="{$operatingSystem['id']|escape}" data-group="{$group|escape}" onclick="ArkHostVPS_ChooseOS(this);return false;">{$operatingSystem['name']|escape}</a>
{/foreach}
</div>
</div>
@@ -23,7 +23,7 @@
<body class="text-center">
<div class="alert alert-warning" role="alert">
<div class="notice">
<img src="{$image}" class="mr-2">{$error}
<img src="{$image}" class="mr-2">{$error|escape}
</div>
</div>
</body>