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 @@
- {!! $message !!} -
+ {{ $message }}Configure firewall rules, policies, aliases, and IPSets for your virtual machine