From 8494f87aae38412e45dc0fc9a37581b676fe4dff Mon Sep 17 00:00:00 2001
From: "Joseph.Rawlings"
Date: Wed, 25 Feb 2026 07:00:55 -0500
Subject: [PATCH] Enhance Proxmox components with input validation, error
handling, and UI improvements
---
.../Proxmox/Livewire/Backups/Create.php | 9 +++-
.../Proxmox/Livewire/Backups/Index.php | 15 ++++---
.../Servers/Proxmox/Livewire/BootOrder.php | 16 +++++++
.../Servers/Proxmox/Livewire/Firewall.php | 18 ++++++++
.../Servers/Proxmox/Livewire/Overview.php | 11 ++++-
.../Servers/Proxmox/Livewire/Reinstall.php | 7 +++
.../Servers/Proxmox/Livewire/Statistics.php | 6 +++
.../Servers/Proxmox/Livewire/Storage.php | 2 +-
.../Servers/Proxmox/Livewire/Subdomain.php | 4 +-
.../Servers/Proxmox/Livewire/System.php | 16 ++++++-
extensions/Servers/Proxmox/Proxmox.php | 38 ++++++++++++----
.../Proxmox/views/boot-order.blade.php | 2 +-
.../Proxmox/views/cloud-init.blade.php | 3 --
.../Proxmox/views/components/layout.blade.php | 14 +++---
.../Servers/Proxmox/views/firewall.blade.php | 16 +++----
.../Servers/Proxmox/views/overview.blade.php | 22 ++++++++--
.../Proxmox/views/statistics.blade.php | 43 +++++++++++--------
.../Servers/Proxmox/views/vnc.blade.php | 2 +-
.../Servers/Proxmox/views/xterm.blade.php | 2 +-
19 files changed, 182 insertions(+), 64 deletions(-)
diff --git a/extensions/Servers/Proxmox/Livewire/Backups/Create.php b/extensions/Servers/Proxmox/Livewire/Backups/Create.php
index 779d9c0b4..dc80ced73 100644
--- a/extensions/Servers/Proxmox/Livewire/Backups/Create.php
+++ b/extensions/Servers/Proxmox/Livewire/Backups/Create.php
@@ -24,7 +24,7 @@ class Create extends Component
}
$backupSettings = $this->server->service->product->settings()->where('key', 'backups')->first();
- $backupLimit = $backupSettings?->value ?? 0;
+ $backupLimit = (int) ($backupSettings?->value ?? 0);
// If limit is 0 or not set, allow unlimited backups
if ($backupLimit <= 0) {
@@ -74,7 +74,7 @@ class Create extends Component
// Check backup limit
$backupSettings = $this->server->service->product->settings()->where('key', 'backups')->first();
- $backupLimit = $backupSettings?->value ?? 0;
+ $backupLimit = (int) ($backupSettings?->value ?? 0);
if ($backupLimit > 0 && !empty($this->server->node->backup_location)) {
try {
@@ -96,6 +96,11 @@ class Create extends Component
}
}
+ if (empty($this->server->node->backup_location)) {
+ $this->notify('Backup storage location is not configured.', 'error');
+ return;
+ }
+
$proxmox = new Proxmox;
try {
diff --git a/extensions/Servers/Proxmox/Livewire/Backups/Index.php b/extensions/Servers/Proxmox/Livewire/Backups/Index.php
index e045d57dd..2d37f2bb8 100644
--- a/extensions/Servers/Proxmox/Livewire/Backups/Index.php
+++ b/extensions/Servers/Proxmox/Livewire/Backups/Index.php
@@ -92,11 +92,16 @@ class Index extends Component
}
}
- $proxmox->request('/nodes/' . $this->server->node->name . '/qemu', 'post', data: [
- 'vmid' => $this->server->vm_id,
- 'force' => true,
- 'archive' => $this->volid,
- ]);
+ try {
+ $proxmox->request('/nodes/' . $this->server->node->name . '/qemu', 'post', data: [
+ 'vmid' => $this->server->vm_id,
+ 'force' => true,
+ 'archive' => $this->volid,
+ ]);
+ } catch (\Throwable $e) {
+ $this->notify('Backup restore failed: ' . $e->getMessage(), 'error');
+ return;
+ }
$this->notify('Backup restoration in progress', 'success');
diff --git a/extensions/Servers/Proxmox/Livewire/BootOrder.php b/extensions/Servers/Proxmox/Livewire/BootOrder.php
index 2bc7d6708..ad5ab2a48 100644
--- a/extensions/Servers/Proxmox/Livewire/BootOrder.php
+++ b/extensions/Servers/Proxmox/Livewire/BootOrder.php
@@ -236,6 +236,12 @@ class BootOrder extends Component
public function removeCdDrive(string $drive)
{
+ // Validate drive format: only ide0-3 or sata0-5 are valid CD/DVD-ROM slots
+ if (!preg_match('/^(ide|sata)\d+$/', $drive)) {
+ $this->notify('Invalid drive identifier.', 'error');
+ return;
+ }
+
if (!in_array($drive, $this->cdDrives, true)) {
$this->notify('Drive not found.', 'error');
@@ -287,6 +293,16 @@ class BootOrder extends Component
: ($this->selectedIso ?: 'none');
$iso = $selectedIso ?: 'none';
+
+ // Validate the selected ISO against available options to prevent arbitrary paths
+ if ($iso !== 'none') {
+ $validIsos = collect($this->isoOptions)->pluck('value')->toArray();
+ if (!in_array($iso, $validIsos, true)) {
+ $this->notify('Invalid ISO selection.', 'error');
+ return;
+ }
+ }
+
$value = $iso === 'none' ? 'none,media=cdrom' : $iso . ',media=cdrom';
try {
diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php
index b9d791c58..50c8cc48d 100644
--- a/extensions/Servers/Proxmox/Livewire/Firewall.php
+++ b/extensions/Servers/Proxmox/Livewire/Firewall.php
@@ -946,6 +946,12 @@ class Firewall extends Component
}
if ($this->editingAliasName) {
+ if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/', $this->editingAliasName)) {
+ $this->error = 'Invalid alias name format';
+ $this->showErrorModal = true;
+ $this->loading = false;
+ return;
+ }
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $this->editingAliasName,
'put',
@@ -1243,6 +1249,12 @@ class Firewall extends Component
$payload['nomatch'] = 1;
}
+ if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/', $this->selectedIPSetName ?? '')) {
+ $this->error = 'Invalid IPSet name format';
+ $this->showErrorModal = true;
+ $this->loading = false;
+ return;
+ }
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $this->selectedIPSetName,
'post',
@@ -1283,6 +1295,12 @@ class Firewall extends Component
return;
}
+ if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/', $this->selectedIPSetName)) {
+ $this->error = 'Invalid IPSet name format';
+ $this->showErrorModal = true;
+ return;
+ }
+
$this->loading = true;
$this->showDeleteIPSetMemberModal = false;
diff --git a/extensions/Servers/Proxmox/Livewire/Overview.php b/extensions/Servers/Proxmox/Livewire/Overview.php
index ee96d4238..cf743189f 100644
--- a/extensions/Servers/Proxmox/Livewire/Overview.php
+++ b/extensions/Servers/Proxmox/Livewire/Overview.php
@@ -78,6 +78,12 @@ class Overview extends Component
protected function fetchGraphData(string $graphType, string $timeRange, string $aggregation)
{
+ $allowedRanges = ['hour', 'day', 'week', 'month', 'year'];
+ $allowedAggs = ['average', 'maximum'];
+ if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) {
+ return;
+ }
+
$cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation;
$data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) {
$proxmox = new Proxmox;
@@ -507,9 +513,10 @@ class Overview extends Component
try {
$proxmox = new Proxmox;
- // Save notes to Proxmox VM description
+ // Save notes to Proxmox VM description (limit to 8KB to avoid bloating Proxmox config)
+ $notes = mb_substr($this->notes ?? '', 0, 8192);
$proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [
- 'description' => $this->notes ?? '',
+ 'description' => $notes,
]);
$this->notify('Notes saved successfully', 'success');
diff --git a/extensions/Servers/Proxmox/Livewire/Reinstall.php b/extensions/Servers/Proxmox/Livewire/Reinstall.php
index d8768f202..c0eba9dcb 100644
--- a/extensions/Servers/Proxmox/Livewire/Reinstall.php
+++ b/extensions/Servers/Proxmox/Livewire/Reinstall.php
@@ -74,6 +74,13 @@ class Reinstall extends Component
public function render()
{
+ if (!$this->server->node) {
+ return view('proxmox::reinstall', [
+ 'osList' => collect(),
+ 'isProtected' => $this->isProtected,
+ ]);
+ }
+
// assuming Server has relation `node()` -> Node model
$locationId = $this->server->node->location_id;
diff --git a/extensions/Servers/Proxmox/Livewire/Statistics.php b/extensions/Servers/Proxmox/Livewire/Statistics.php
index bf562f876..4f1d71112 100644
--- a/extensions/Servers/Proxmox/Livewire/Statistics.php
+++ b/extensions/Servers/Proxmox/Livewire/Statistics.php
@@ -858,6 +858,12 @@ class Statistics extends Component
protected function fetchGraphData(string $graphType, string $timeRange, string $aggregation, bool $forceRefresh = false): void
{
+ $allowedRanges = ['hour', 'day', 'week', 'month', 'year'];
+ $allowedAggs = ['average', 'maximum'];
+ if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) {
+ return;
+ }
+
$cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation;
try {
diff --git a/extensions/Servers/Proxmox/Livewire/Storage.php b/extensions/Servers/Proxmox/Livewire/Storage.php
index 1e2bfb091..0f7ebc4fb 100644
--- a/extensions/Servers/Proxmox/Livewire/Storage.php
+++ b/extensions/Servers/Proxmox/Livewire/Storage.php
@@ -1059,7 +1059,7 @@ class Storage extends Component
// Find unused disks and delete the one we just detached
foreach ($config as $key => $value) {
- if (str_starts_with($key, 'unused') && str_contains($value, $this->drives[$driveKey]['volume'])) {
+ if (str_starts_with($key, 'unused') && trim($value) === $this->drives[$driveKey]['volume']) {
$proxmox->request(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
'put',
diff --git a/extensions/Servers/Proxmox/Livewire/Subdomain.php b/extensions/Servers/Proxmox/Livewire/Subdomain.php
index 445802bb4..e6b97157f 100644
--- a/extensions/Servers/Proxmox/Livewire/Subdomain.php
+++ b/extensions/Servers/Proxmox/Livewire/Subdomain.php
@@ -498,8 +498,8 @@ class Subdomain extends Component
$this->recordName = '';
$this->recordRdata = '';
- // Small delay to ensure API has processed the record
- usleep(500000); // 0.5 seconds
+ // Brief yield to allow DNS API to process the record
+ usleep(100000); // 100ms — minimal delay for API propagation
// Reload records from DNS API to get the latest data
$this->loadZoneRecords();
diff --git a/extensions/Servers/Proxmox/Livewire/System.php b/extensions/Servers/Proxmox/Livewire/System.php
index 301de0eab..ef3a9e935 100644
--- a/extensions/Servers/Proxmox/Livewire/System.php
+++ b/extensions/Servers/Proxmox/Livewire/System.php
@@ -224,7 +224,8 @@ class System extends Component
}
}
- return $value;
+ // Reject unknown/unrecognised values to prevent arbitrary strings reaching the API
+ return '';
}
protected function fetchCpuOptions(): array
@@ -333,8 +334,10 @@ class System extends Component
$resolvedCpu .= ',flags=' . $flags;
}
+ $resolvedMachine = $this->machineType !== '' ? $this->resolveMachineValue($this->machineType) : null;
+
$payload = array_filter([
- 'machine' => $this->machineType !== '' ? $this->resolveMachineValue($this->machineType) : null,
+ 'machine' => $resolvedMachine !== '' ? $resolvedMachine : null,
'bios' => $this->biosType !== '' ? $this->biosType : null,
'cpu' => $resolvedCpu,
'kvm' => $this->kvmEnabled ? 1 : 0,
@@ -434,6 +437,15 @@ class System extends Component
return;
}
+ // Only allow removal of EFI disk and TPM devices via this method
+ $isAllowed = (str_starts_with($deviceKey, 'efidisk') && isset($this->efiDisks[$deviceKey]))
+ || (str_starts_with($deviceKey, 'tpmstate') && isset($this->tpmDevices[$deviceKey]));
+
+ if (!$isAllowed) {
+ $this->notify('Only EFI disk and TPM devices can be removed here.', 'error');
+ return;
+ }
+
// Check VM status - must be stopped to remove EFI/TPM devices
if (!$this->status || $this->status->status !== 'stopped') {
$this->notify('VM must be stopped to remove system devices.', 'error');
diff --git a/extensions/Servers/Proxmox/Proxmox.php b/extensions/Servers/Proxmox/Proxmox.php
index ff00eef7b..2a84ae668 100644
--- a/extensions/Servers/Proxmox/Proxmox.php
+++ b/extensions/Servers/Proxmox/Proxmox.php
@@ -376,7 +376,11 @@ class Proxmox extends ClassServer
public function getCheckoutConfig(Product $product)
{
- $oses = Location::findOrFail($product->settings()->where('key', 'location_id')->first()->value)->os()->get();
+ $locationSetting = $product->settings()->where('key', 'location_id')->first();
+ if (!$locationSetting) {
+ return [['name' => 'notice', 'type' => 'placeholder', 'label' => new \Illuminate\Support\HtmlString('Product is missing a location_id setting. Please contact support.')]];
+ }
+ $oses = Location::findOrFail($locationSetting->value)->os()->get();
return [
[
@@ -442,8 +446,9 @@ class Proxmox extends ClassServer
private function generateMacAddress(): string
{
$mac = [];
+ $bytes = random_bytes(6);
for ($i = 0; $i < 6; $i++) {
- $mac[] = str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT);
+ $mac[] = str_pad(dechex(ord($bytes[$i])), 2, '0', STR_PAD_LEFT);
}
// Get the first octet as an integer
@@ -527,13 +532,18 @@ class Proxmox extends ClassServer
// Get current disk type
$disk = $this->request('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/config')->json()['data'] ?? [];
- // As the disk type can be different, we need to check if it is scsi or virtio or sata or ide
- $diskType = 'scsi0';
- if (isset($disk['virtio0'])) {
- $diskType = 'virtio0';
- } elseif (isset($disk['sata0'])) {
- $diskType = 'sata0';
+ // Find the primary disk across all bus types and slot numbers
+ $diskType = null;
+ foreach (['scsi', 'virtio', 'sata', 'ide'] as $bus) {
+ for ($i = 0; $i < 4; $i++) {
+ $key = $bus . $i;
+ if (isset($disk[$key]) && !str_contains($disk[$key], 'media=cdrom') && !str_contains(strtolower($disk[$key]), 'cloudinit')) {
+ $diskType = $key;
+ break 2;
+ }
+ }
}
+ $diskType = $diskType ?? 'scsi0';
// Update disk
$this->requestAndWait('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/resize', 'put', [
@@ -788,7 +798,7 @@ class Proxmox extends ClassServer
'verify' => $this->config('verify_ssl') ? true : false,
])->post(rtrim($this->config('host'), '/') . '/api2/json/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/vncproxy', [
'websocket' => 1,
- ])->json()['data'] ?? [];
+ ])->throw()->json()['data'] ?? [];
// Current
$paymenterDomain = rtrim(config('settings.app_url'), '/');
@@ -974,6 +984,16 @@ class Proxmox extends ClassServer
return false;
}
+ if ($server->node) {
+ try {
+ $this->requestAndWait('/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/status/start', 'post', [
+ 'vmid' => $server->vm_id,
+ ], $server->node->name);
+ } catch (\Throwable $e) {
+ \Log::warning('Failed to start VM on unsuspend', ['server' => $server->id, 'error' => $e->getMessage()]);
+ }
+ }
+
$server->status = 'active';
$server->save();
diff --git a/extensions/Servers/Proxmox/views/boot-order.blade.php b/extensions/Servers/Proxmox/views/boot-order.blade.php
index a4852bb03..9c326b55e 100644
--- a/extensions/Servers/Proxmox/views/boot-order.blade.php
+++ b/extensions/Servers/Proxmox/views/boot-order.blade.php
@@ -262,7 +262,7 @@
-
-
-
-
diff --git a/extensions/Servers/Proxmox/views/components/layout.blade.php b/extensions/Servers/Proxmox/views/components/layout.blade.php
index 23fe6b825..9864570b3 100644
--- a/extensions/Servers/Proxmox/views/components/layout.blade.php
+++ b/extensions/Servers/Proxmox/views/components/layout.blade.php
@@ -3127,9 +3127,7 @@
$message = $statuses[$server->status]['message'] ?? "This server is currently inactive. Please view the Services tab in the Dashboard to see more information about the current state of this server. If more information is required, please contact support via the support portal under Tickets. This message can be displayed after a service is suspended, cancelled, revoked, or potentially in error. If you believe this message has been displayed in error, please contact us via a ticket and be sure to include the following information: VM{$server->vm_id}-ID{$server->id}-{$server->status}.";
@endphp
-