From 6197558a38f5afbb74f138be59b52e705c99bf1e Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:08:48 +0530 Subject: [PATCH 01/12] fix(validation): add input validation for resource limit fields --- .../Project/Shared/ResourceLimits.php | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index 0b3840289..8a14dc10c 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Shared; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Validation\ValidationException; use Livewire\Component; class ResourceLimits extends Component @@ -16,24 +17,24 @@ class ResourceLimits extends Component public ?string $limitsCpuset = null; - public ?int $limitsCpuShares = null; + public mixed $limitsCpuShares = null; public string $limitsMemory; public string $limitsMemorySwap; - public int $limitsMemorySwappiness; + public mixed $limitsMemorySwappiness = 0; public string $limitsMemoryReservation; protected $rules = [ - 'limitsMemory' => 'required|string', - 'limitsMemorySwap' => 'required|string', + 'limitsMemory' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'], + 'limitsMemorySwap' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'], 'limitsMemorySwappiness' => 'required|integer|min:0|max:100', - 'limitsMemoryReservation' => 'required|string', - 'limitsCpus' => 'nullable', - 'limitsCpuset' => 'nullable', - 'limitsCpuShares' => 'nullable', + 'limitsMemoryReservation' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'], + 'limitsCpus' => ['nullable', 'regex:/^\d*\.?\d+$/'], + 'limitsCpuset' => ['nullable', 'regex:/^\d+([,-]\d+)*$/'], + 'limitsCpuShares' => 'nullable|integer|min:0', ]; protected $validationAttributes = [ @@ -46,6 +47,19 @@ class ResourceLimits extends Component 'limitsCpuShares' => 'cpu shares', ]; + protected $messages = [ + 'limitsMemory.regex' => 'Maximum Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.', + 'limitsMemorySwap.regex' => 'Maximum Swap Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.', + 'limitsMemoryReservation.regex' => 'Soft Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.', + 'limitsCpus.regex' => 'Number of CPUs must be a number (integer or decimal). Example: 0.5, 2.', + 'limitsCpuset.regex' => 'CPU sets must be a comma-separated list of CPU numbers or ranges. Example: 0-2 or 0,1,3.', + 'limitsMemorySwappiness.integer' => 'Swappiness must be a whole number between 0 and 100.', + 'limitsMemorySwappiness.min' => 'Swappiness must be between 0 and 100.', + 'limitsMemorySwappiness.max' => 'Swappiness must be between 0 and 100.', + 'limitsCpuShares.integer' => 'CPU Weight must be a whole number.', + 'limitsCpuShares.min' => 'CPU Weight must be a positive number.', + ]; + /** * Sync data between component properties and model * @@ -57,10 +71,10 @@ private function syncData(bool $toModel = false): void // Sync TO model (before save) $this->resource->limits_cpus = $this->limitsCpus; $this->resource->limits_cpuset = $this->limitsCpuset; - $this->resource->limits_cpu_shares = $this->limitsCpuShares; + $this->resource->limits_cpu_shares = (int) $this->limitsCpuShares; $this->resource->limits_memory = $this->limitsMemory; $this->resource->limits_memory_swap = $this->limitsMemorySwap; - $this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness; + $this->resource->limits_memory_swappiness = (int) $this->limitsMemorySwappiness; $this->resource->limits_memory_reservation = $this->limitsMemoryReservation; } else { // Sync FROM model (on load/refresh) @@ -91,7 +105,7 @@ public function submit() if (! $this->limitsMemorySwap) { $this->limitsMemorySwap = '0'; } - if (is_null($this->limitsMemorySwappiness)) { + if ($this->limitsMemorySwappiness === '' || is_null($this->limitsMemorySwappiness)) { $this->limitsMemorySwappiness = 60; } if (! $this->limitsMemoryReservation) { @@ -103,7 +117,7 @@ public function submit() if ($this->limitsCpuset === '') { $this->limitsCpuset = null; } - if (is_null($this->limitsCpuShares)) { + if ($this->limitsCpuShares === '' || is_null($this->limitsCpuShares)) { $this->limitsCpuShares = 1024; } @@ -112,6 +126,12 @@ public function submit() $this->syncData(true); $this->resource->save(); $this->dispatch('success', 'Resource limits updated.'); + } catch (ValidationException $e) { + foreach ($e->validator->errors()->all() as $message) { + $this->dispatch('error', $message); + } + + return; } catch (\Throwable $e) { return handleError($e, $this); } From 407b6df7440d90f324a578ee0b0ebd10cae1da6a Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:30:17 +0530 Subject: [PATCH 02/12] fix(validation): add IP validation for custom DNS servers input --- app/Livewire/Settings/Advanced.php | 4 ++-- app/Rules/ValidDnsServers.php | 35 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 app/Rules/ValidDnsServers.php diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php index ad478273f..f4b57ae20 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -3,6 +3,7 @@ namespace App\Livewire\Settings; use App\Models\InstanceSettings; +use App\Rules\ValidDnsServers; use App\Rules\ValidIpOrCidr; use Livewire\Attributes\Validate; use Livewire\Component; @@ -20,7 +21,6 @@ class Advanced extends Component #[Validate('boolean')] public bool $is_dns_validation_enabled; - #[Validate('nullable|string')] public ?string $custom_dns_servers = null; #[Validate('boolean')] @@ -43,7 +43,7 @@ public function rules() 'is_registration_enabled' => 'boolean', 'do_not_track' => 'boolean', 'is_dns_validation_enabled' => 'boolean', - 'custom_dns_servers' => 'nullable|string', + 'custom_dns_servers' => ['nullable', 'string', new ValidDnsServers], 'is_api_enabled' => 'boolean', 'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr], 'is_sponsorship_popup_enabled' => 'boolean', diff --git a/app/Rules/ValidDnsServers.php b/app/Rules/ValidDnsServers.php new file mode 100644 index 000000000..e3bbd048f --- /dev/null +++ b/app/Rules/ValidDnsServers.php @@ -0,0 +1,35 @@ + Date: Sat, 28 Mar 2026 23:23:25 +0530 Subject: [PATCH 03/12] fix(validation): add input validation for port exposes and port mappings fields --- app/Livewire/Project/Application/General.php | 12 ++++++++-- .../Project/Database/Clickhouse/General.php | 6 ++++- .../Project/Database/Dragonfly/General.php | 6 ++++- .../Project/Database/Keydb/General.php | 6 ++++- .../Project/Database/Mariadb/General.php | 6 ++++- .../Project/Database/Mongodb/General.php | 6 ++++- .../Project/Database/Mysql/General.php | 6 ++++- .../Project/Database/Postgresql/General.php | 6 ++++- .../Project/Database/Redis/General.php | 6 ++++- app/Support/ValidationPatterns.php | 24 +++++++++++++++++++ 10 files changed, 74 insertions(+), 10 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 5c186af70..3e78c1732 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -153,8 +153,8 @@ protected function rules(): array 'staticImage' => 'required', 'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)), 'publishDirectory' => ValidationPatterns::directoryPathRules(), - 'portsExposes' => 'required', - 'portsMappings' => 'nullable', + 'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'], + 'portsMappings' => ValidationPatterns::portMappingRules(), 'customNetworkAliases' => 'nullable', 'dockerfile' => 'nullable', 'dockerRegistryImageName' => 'nullable', @@ -209,6 +209,8 @@ protected function messages(): array 'staticImage.required' => 'The Static Image field is required.', 'baseDirectory.required' => 'The Base Directory field is required.', 'portsExposes.required' => 'The Exposed Ports field is required.', + 'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).', + ...ValidationPatterns::portMappingMessages(), 'isStatic.required' => 'The Static setting is required.', 'isStatic.boolean' => 'The Static setting must be true or false.', 'isSpa.required' => 'The SPA setting is required.', @@ -752,6 +754,12 @@ public function submit($showToaster = true) $this->authorize('update', $this->application); $this->resetErrorBag(); + + $this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString(); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } + $this->validate(); $oldPortsExposes = $this->application->ports_exposes; diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 9de75c1c5..0913ca797 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -79,7 +79,7 @@ protected function rules(): array 'clickhouseAdminUser' => 'required|string', 'clickhouseAdminPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -94,6 +94,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'clickhouseAdminUser.required' => 'The Admin User field is required.', 'clickhouseAdminUser.string' => 'The Admin User must be a string.', @@ -207,6 +208,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d35e57a9d..23503bd98 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -90,7 +90,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'dragonflyPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -106,6 +106,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'dragonflyPassword.required' => 'The Dragonfly Password field is required.', 'dragonflyPassword.string' => 'The Dragonfly Password must be a string.', @@ -217,6 +218,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index adb4ccb5f..ff9dc19ad 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -93,7 +93,7 @@ protected function rules(): array 'keydbConf' => 'nullable|string', 'keydbPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -111,6 +111,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'keydbPassword.required' => 'The KeyDB Password field is required.', 'keydbPassword.string' => 'The KeyDB Password must be a string.', @@ -224,6 +225,9 @@ public function submit() try { $this->authorize('manageEnvironment', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 14240c82d..a9ac47b97 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -78,7 +78,7 @@ protected function rules(): array 'mariadbDatabase' => 'required', 'mariadbConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mariadbRootPassword.required' => 'The Root Password field is required.', @@ -213,6 +214,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 11419ec71..2b6538edf 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -77,7 +77,7 @@ protected function rules(): array 'mongoInitdbRootPassword' => 'required', 'mongoInitdbDatabase' => 'required', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mongoInitdbRootUsername.required' => 'The Root Username field is required.', @@ -213,6 +214,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 4f0f5eb19..f3c554522 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -80,7 +80,7 @@ protected function rules(): array 'mysqlDatabase' => 'required', 'mysqlConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -95,6 +95,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mysqlRootPassword.required' => 'The Root Password field is required.', @@ -220,6 +221,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 4e044672b..7a4ff057e 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -92,7 +92,7 @@ protected function rules(): array 'postgresConf' => 'nullable', 'initScripts' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -107,6 +107,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'postgresUser.required' => 'The Postgres User field is required.', @@ -456,6 +457,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index ebe2f3ba0..a1a22ab8b 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -73,7 +73,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'redisConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -89,6 +89,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'image.required' => 'The Docker Image field is required.', @@ -201,6 +202,9 @@ public function submit() try { $this->authorize('manageEnvironment', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } $this->syncData(true); if (version_compare($this->redisVersion, '6.0', '>=')) { diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 7084b4cc2..5d53076ea 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -194,6 +194,12 @@ public static function volumeNameMessages(string $field = 'name'): array ]; } + /** + * Pattern for port mappings (e.g. 3000:3000, 8080:80, 8000-8010:8000-8010) + * Each entry requires host:container format, where each side can be a number or a range (number-number) + */ + public const PORT_MAPPINGS_PATTERN = '/^(\d+(-\d+)?:\d+(-\d+)?)(,\d+(-\d+)?:\d+(-\d+)?)*$/'; + /** * Get validation rules for container name fields */ @@ -202,6 +208,24 @@ public static function containerNameRules(int $maxLength = 255): array return ['string', 'max:'.$maxLength, 'regex:'.self::CONTAINER_NAME_PATTERN]; } + /** + * Get validation rules for port mapping fields + */ + public static function portMappingRules(): array + { + return ['nullable', 'string', 'regex:'.self::PORT_MAPPINGS_PATTERN]; + } + + /** + * Get validation messages for port mapping fields + */ + public static function portMappingMessages(string $field = 'portsMappings'): array + { + return [ + "{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges (e.g. 3000:3000,8080:80,8000-8010:8000-8010).', + ]; + } + /** * Check if a string is a valid Docker container name. */ From 73258c317e3d79aefb90924d5319ddc54209eebc Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:34:32 +0530 Subject: [PATCH 04/12] fix(validation): add URL validation for proxy redirect input --- app/Livewire/Server/Proxy.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index d5f30fca0..c2d8205ef 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -6,6 +6,7 @@ use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; use App\Models\Server; +use App\Rules\SafeExternalUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -41,9 +42,13 @@ public function getListeners() ]; } - protected $rules = [ - 'generateExactLabels' => 'required|boolean', - ]; + protected function rules() + { + return [ + 'generateExactLabels' => 'required|boolean', + 'redirectUrl' => ['nullable', new SafeExternalUrl], + ]; + } public function mount() { @@ -147,6 +152,7 @@ public function submit() { try { $this->authorize('update', $this->server); + $this->validate(); SaveProxyConfiguration::run($this->server, $this->proxySettings); $this->server->proxy->redirect_url = $this->redirectUrl; $this->server->save(); From c52a199120d2d06e2a473305734643f2bd66ead1 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:14:08 +0530 Subject: [PATCH 05/12] fix(validation): add input validation for server advanced settings page --- app/Http/Controllers/Api/ServersController.php | 18 +++++++++++++++++- app/Livewire/Server/Advanced.php | 16 ++++++++-------- .../views/livewire/server/advanced.blade.php | 4 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 2ef95ce8b..930879d80 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -598,6 +598,11 @@ public function create_server(Request $request) 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'], 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'], 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'], + 'concurrent_builds' => ['type' => 'integer', 'description' => 'Number of concurrent builds.'], + 'dynamic_timeout' => ['type' => 'integer', 'description' => 'Deployment timeout in seconds.'], + 'deployment_queue_limit' => ['type' => 'integer', 'description' => 'Maximum number of queued deployments.'], + 'server_disk_usage_notification_threshold' => ['type' => 'integer', 'description' => 'Server disk usage notification threshold (%).'], + 'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'], ], ), ), @@ -634,7 +639,7 @@ public function create_server(Request $request) )] public function update_server(Request $request) { - $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type']; + $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -655,6 +660,11 @@ public function update_server(Request $request) 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', + 'concurrent_builds' => 'integer|nullable|min:1', + 'dynamic_timeout' => 'integer|nullable|min:1', + 'deployment_queue_limit' => 'integer|nullable|min:1', + 'server_disk_usage_notification_threshold' => 'integer|nullable|min:1|max:100', + 'server_disk_usage_check_frequency' => 'string|nullable', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -691,6 +701,12 @@ public function update_server(Request $request) 'is_build_server' => $request->is_build_server, ]); } + + $advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']); + if (! empty($advancedSettings)) { + $server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value))); + } + if ($request->instant_validate) { ValidateServer::dispatch($server); } diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index dba1b4903..0e1a9a325 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -15,17 +15,17 @@ class Advanced extends Component #[Validate(['string'])] public string $serverDiskUsageCheckFrequency = '0 23 * * *'; - #[Validate(['integer', 'min:1', 'max:99'])] - public int $serverDiskUsageNotificationThreshold = 50; + #[Validate(['required', 'integer', 'min:1', 'max:99'])] + public ?int $serverDiskUsageNotificationThreshold = 50; - #[Validate(['integer', 'min:1'])] - public int $concurrentBuilds = 1; + #[Validate(['required', 'integer', 'min:1'])] + public ?int $concurrentBuilds = 1; - #[Validate(['integer', 'min:1'])] - public int $dynamicTimeout = 1; + #[Validate(['required', 'integer', 'min:1'])] + public ?int $dynamicTimeout = 1; - #[Validate(['integer', 'min:1'])] - public int $deploymentQueueLimit = 25; + #[Validate(['required', 'integer', 'min:1'])] + public ?int $deploymentQueueLimit = 25; public function mount(string $server_uuid) { diff --git a/resources/views/livewire/server/advanced.blade.php b/resources/views/livewire/server/advanced.blade.php index 33086aea1..f6610c1d5 100644 --- a/resources/views/livewire/server/advanced.blade.php +++ b/resources/views/livewire/server/advanced.blade.php @@ -22,6 +22,7 @@ id="serverDiskUsageCheckFrequency" label="Disk usage check frequency" required helper="Cron expression for disk usage check frequency.
You can use every_minute, hourly, daily, weekly, monthly, yearly.

Default is every night at 11:00 PM." /> @@ -31,12 +32,15 @@

Builds

From 15a98b52c93746e54bd40812f0a9f8c0229a5457 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:24:08 +0530 Subject: [PATCH 06/12] fix(validation): add input validation for server_disk_usage_check_frequency on API --- app/Http/Controllers/Api/ServersController.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 930879d80..beba33a8c 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -702,6 +702,13 @@ public function update_server(Request $request) ]); } + if ($request->has('server_disk_usage_check_frequency') && ! validate_cron_expression($request->server_disk_usage_check_frequency)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_disk_usage_check_frequency' => ['Invalid Cron / Human expression for Disk Usage Check Frequency.']], + ], 422); + } + $advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']); if (! empty($advancedSettings)) { $server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value))); From 1ebba7da3acccf7e0cb1cf740ec531637895276b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:52:19 +0530 Subject: [PATCH 07/12] fix(validation): add input validation for sentinel configuration --- app/Livewire/Server/Sentinel.php | 6 +++--- resources/views/livewire/server/sentinel.blade.php | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index dff379ae1..a4b35891b 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -25,13 +25,13 @@ class Sentinel extends Component public ?string $sentinelUpdatedAt = null; #[Validate(['required', 'integer', 'min:1'])] - public int $sentinelMetricsRefreshRateSeconds; + public int|string $sentinelMetricsRefreshRateSeconds; #[Validate(['required', 'integer', 'min:1'])] - public int $sentinelMetricsHistoryDays; + public int|string $sentinelMetricsHistoryDays; #[Validate(['required', 'integer', 'min:10'])] - public int $sentinelPushIntervalSeconds; + public int|string $sentinelPushIntervalSeconds; #[Validate(['nullable', 'url'])] public ?string $sentinelCustomUrl = null; diff --git a/resources/views/livewire/server/sentinel.blade.php b/resources/views/livewire/server/sentinel.blade.php index 4016a30e4..5ca535cbc 100644 --- a/resources/views/livewire/server/sentinel.blade.php +++ b/resources/views/livewire/server/sentinel.blade.php @@ -91,13 +91,14 @@
- - -
From 791aa10b3fed851df81192538f2aa15144eb179b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:24:36 +0530 Subject: [PATCH 08/12] fix(validation): use int|string for Livewire numeric properties and remove nullable from API rules --- app/Http/Controllers/Api/ServersController.php | 10 +++++----- app/Livewire/Server/Advanced.php | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index beba33a8c..c13c6665c 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -660,11 +660,11 @@ public function update_server(Request $request) 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', - 'concurrent_builds' => 'integer|nullable|min:1', - 'dynamic_timeout' => 'integer|nullable|min:1', - 'deployment_queue_limit' => 'integer|nullable|min:1', - 'server_disk_usage_notification_threshold' => 'integer|nullable|min:1|max:100', - 'server_disk_usage_check_frequency' => 'string|nullable', + 'concurrent_builds' => 'integer|min:1', + 'dynamic_timeout' => 'integer|min:1', + 'deployment_queue_limit' => 'integer|min:1', + 'server_disk_usage_notification_threshold' => 'integer|min:1|max:100', + 'server_disk_usage_check_frequency' => 'string', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index 0e1a9a325..b39da5e5a 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -16,16 +16,16 @@ class Advanced extends Component public string $serverDiskUsageCheckFrequency = '0 23 * * *'; #[Validate(['required', 'integer', 'min:1', 'max:99'])] - public ?int $serverDiskUsageNotificationThreshold = 50; + public int|string $serverDiskUsageNotificationThreshold = 50; #[Validate(['required', 'integer', 'min:1'])] - public ?int $concurrentBuilds = 1; + public int|string $concurrentBuilds = 1; #[Validate(['required', 'integer', 'min:1'])] - public ?int $dynamicTimeout = 1; + public int|string $dynamicTimeout = 1; #[Validate(['required', 'integer', 'min:1'])] - public ?int $deploymentQueueLimit = 25; + public int|string $deploymentQueueLimit = 25; public function mount(string $server_uuid) { From 67f8eb929f5655a71263c99c2c05a27654a8868b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:48:32 +0530 Subject: [PATCH 09/12] fix(validation): add input validation for database backup timeout --- app/Livewire/Project/Database/BackupEdit.php | 2 +- .../project/database/backup-edit.blade.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 0fff2bd03..a18022882 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -76,7 +76,7 @@ class BackupEdit extends Component public bool $dumpAll = false; #[Validate(['required', 'int', 'min:60', 'max:36000'])] - public int $timeout = 3600; + public int|string $timeout = 3600; public function mount() { diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index bb5dcfc4d..d5c25916a 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -81,10 +81,10 @@ @endif
- + - + helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required /> +

Backup Retention Settings

@@ -101,13 +101,13 @@
+ helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required /> + helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." required /> + helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." required />
@@ -117,13 +117,13 @@
+ helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required /> + helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." required /> + helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." required />
@endif From 40420e33e3aa138981cbd22982dfcc7eef2d9644 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:53:18 +0530 Subject: [PATCH 10/12] fix(validation): add timeout validation to database backup API endpoints --- app/Http/Controllers/Api/DatabasesController.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 660ed4529..a73bde1ae 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -641,6 +641,7 @@ public function update_by_uuid(Request $request) 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Number of backups to retain in S3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Number of days to retain backups in S3'], 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage (MB) for S3 backups'], + 'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600], ], ), ) @@ -677,7 +678,7 @@ public function update_by_uuid(Request $request) )] public function create_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -704,6 +705,7 @@ public function create_backup(Request $request) 'database_backup_retention_amount_s3' => 'integer|min:0', 'database_backup_retention_days_s3' => 'integer|min:0', 'database_backup_retention_max_storage_s3' => 'integer|min:0', + 'timeout' => 'integer|min:60|max:36000', ]); if ($validator->fails()) { @@ -878,6 +880,7 @@ public function create_backup(Request $request) 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'], + 'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600], ], ), ) @@ -907,7 +910,7 @@ public function create_backup(Request $request) )] public function update_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -932,6 +935,7 @@ public function update_backup(Request $request) 'database_backup_retention_amount_s3' => 'integer|min:0', 'database_backup_retention_days_s3' => 'integer|min:0', 'database_backup_retention_max_storage_s3' => 'integer|min:0', + 'timeout' => 'integer|min:60|max:36000', ]); if ($validator->fails()) { return response()->json([ From b98346f3c3ffbc14868bf4b0e7b5f1c73da81dd4 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 03:02:15 +0530 Subject: [PATCH 11/12] fix(validation): validate cron expressions in update backup API endpoint --- app/Http/Controllers/Api/DatabasesController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a73bde1ae..856649b91 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -928,7 +928,7 @@ public function update_backup(Request $request) 'dump_all' => 'boolean', 's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable', 'databases_to_backup' => 'string|nullable', - 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'frequency' => 'string', 'database_backup_retention_amount_locally' => 'integer|min:0', 'database_backup_retention_days_locally' => 'integer|min:0', 'database_backup_retention_max_storage_locally' => 'integer|min:0', @@ -962,6 +962,17 @@ public function update_backup(Request $request) $this->authorize('update', $database); + // Validate frequency is a valid cron expression + if ($request->filled('frequency')) { + $isValid = validate_cron_expression($request->frequency); + if (! $isValid) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['frequency' => ['Invalid cron expression or frequency format.']], + ], 422); + } + } + if ($request->boolean('save_s3') && ! $request->filled('s3_storage_uuid')) { return response()->json([ 'message' => 'Validation failed.', From 1daff4e23ce9205241cc65459e2191f164404ebe Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:13:30 +0530 Subject: [PATCH 12/12] fix(validation): add input validation for emails configuration --- app/Livewire/Notifications/Email.php | 4 ++-- app/Livewire/SettingsEmail.php | 4 ++-- resources/views/livewire/notifications/email.blade.php | 4 ++-- resources/views/livewire/settings-email.blade.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 847f10765..364163ff8 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -42,7 +42,7 @@ class Email extends Component public ?string $smtpHost = null; #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] - public ?int $smtpPort = null; + public ?string $smtpPort = null; #[Validate(['nullable', 'string', 'in:starttls,tls,none'])] public ?string $smtpEncryption = null; @@ -54,7 +54,7 @@ class Email extends Component public ?string $smtpPassword = null; #[Validate(['nullable', 'numeric'])] - public ?int $smtpTimeout = null; + public ?string $smtpTimeout = null; #[Validate(['boolean'])] public bool $resendEnabled = false; diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index ca48e9b16..8c0e24400 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -33,7 +33,7 @@ class SettingsEmail extends Component public ?string $smtpHost = null; #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] - public ?int $smtpPort = null; + public ?string $smtpPort = null; #[Validate(['nullable', 'string', 'in:starttls,tls,none'])] public ?string $smtpEncryption = 'starttls'; @@ -45,7 +45,7 @@ class SettingsEmail extends Component public ?string $smtpPassword = null; #[Validate(['nullable', 'numeric'])] - public ?int $smtpTimeout = null; + public ?string $smtpTimeout = null; #[Validate(['boolean'])] public bool $resendEnabled = false; diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 538851137..410703010 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -72,7 +72,7 @@ class="p-4 border dark:border-coolgray-300 border-neutral-200 rounded-lg flex fl
- + @@ -82,7 +82,7 @@ class="p-4 border dark:border-coolgray-300 border-neutral-200 rounded-lg flex fl
-
diff --git a/resources/views/livewire/settings-email.blade.php b/resources/views/livewire/settings-email.blade.php index c58ea189d..93abd628c 100644 --- a/resources/views/livewire/settings-email.blade.php +++ b/resources/views/livewire/settings-email.blade.php @@ -53,7 +53,7 @@ - +