CVE-2024-34067: Insufficient Information (NVD-CWE-noinfo)
value: Pterodactyl is a free, open-source game server management panel built with PHP, React, and Go. Importing a malicious egg or gaining access to wings instance could lead to cross site scripting (XSS) on the panel, which could be used to gain an administrator account on the panel. Specifically, the following things are impacted: Egg Docker images and Egg variables: Name, Environment variable, Default value, Description, Validation rules. Additionally, certain fields would reflect malicious input, but it would require the user knowingly entering such input to have an impact. To iterate, this would require an administrator to perform actions and can't be triggered by a normal panel user. This issue has has been addressed in version 1.11.6 and users are advised to upgrade. No workaround is available other than updating to the latest version of the panel.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-03T18:15Z
Repository: https://github.com/pterodactyl/panel
Language: PHP
Files changed: app/Http/Controllers/Admin/Nests/EggVariableController.php, app/Http/Controllers/Admin/Nests/NestController.php, app/Http/Controllers/Admin/NodesController.php, app/Http/Requests/Admin/Egg/EggFormRequest.php, app/Http/Requests/Admin/Nest/StoreNestFormRequest.php, app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php, app/Models/Egg.php, app/Models/Server.php, public/themes/pterodactyl/js/admin/new-server.js, resources/views/admin/servers/view/startup.blade.php
Commit date: 2024-04-10 18:13:25 -0600
Commit message: admin: tweaks to validation and rendering

Diff: diff --git a/app/Http/Controllers/Admin/Nests/EggVariableController.php b/app/Http/Controllers/Admin/Nests/EggVariableController.php
index 40274b323..2b84e8b02 100644
--- a/app/Http/Controllers/Admin/Nests/EggVariableController.php
+++ b/app/Http/Controllers/Admin/Nests/EggVariableController.php
@@ -67,11 +67,11 @@ class EggVariableController extends Controller
      */
     public function update(EggVariableFormRequest $request, Egg $egg, EggVariable $variable): RedirectResponse
     {
         $this->updateService->handle($variable, $request->normalize());
         $this->alert->success(trans('admin/nests.variables.notices.variable_updated', [
-            'variable' => $variable->name,
+            'variable' => htmlspecialchars($variable->name),
         ]))->flash();
 
         return redirect()->route('admin.nests.egg.variables', $egg->id);
     }
 
@@ -80,11 +80,11 @@ class EggVariableController extends Controller
      */
     public function destroy(int $egg, EggVariable $variable): RedirectResponse
     {
         $this->variableRepository->delete($variable->id);
         $this->alert->success(trans('admin/nests.variables.notices.variable_deleted', [
-            'variable' => $variable->name,
+            'variable' => htmlspecialchars($variable->name),
         ]))->flash();
 
         return redirect()->route('admin.nests.egg.variables', $egg);
     }
 }
diff --git a/app/Http/Controllers/Admin/Nests/NestController.php b/app/Http/Controllers/Admin/Nests/NestController.php
index 037dd0943..311a5df28 100644
--- a/app/Http/Controllers/Admin/Nests/NestController.php
+++ b/app/Http/Controllers/Admin/Nests/NestController.php
@@ -54,11 +54,11 @@ class NestController extends Controller
      * @throws \Pterodactyl\Exceptions\Model\DataValidationException
      */
     public function store(StoreNestFormRequest $request): RedirectResponse
     {
         $nest = $this->nestCreationService->handle($request->normalize());
-        $this->alert->success(trans('admin/nests.notices.created', ['name' => $nest->name]))->flash();
+        $this->alert->success(trans('admin/nests.notices.created', ['name' => htmlspecialchars($nest->name)]))->flash();
 
         return redirect()->route('admin.nests.view', $nest->id);
     }
 
     /**
diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php
index 573a1d9f8..71094a517 100644
--- a/app/Http/Controllers/Admin/NodesController.php
+++ b/app/Http/Controllers/Admin/NodesController.php
@@ -129,11 +129,11 @@ class NodesController extends Controller
             ['node_id', '=', $node],
             ['server_id', '=', null],
             ['ip', '=', $request->input('ip')],
         ]);
 
-        $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
+        $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => htmlspecialchars($request->input('ip'))]))
             ->flash();
 
         return redirect()->route('admin.nodes.view.allocation', $node);
     }
 
diff --git a/app/Http/Requests/Admin/Egg/EggFormRequest.php b/app/Http/Requests/Admin/Egg/EggFormRequest.php
index a41ac4eba..0a88f5a74 100644
--- a/app/Http/Requests/Admin/Egg/EggFormRequest.php
+++ b/app/Http/Requests/Admin/Egg/EggFormRequest.php
@@ -9,11 +9,11 @@ class EggFormRequest extends AdminFormRequest
     public function rules(): array
     {
         $rules = [
             'name' => 'required|string|max:191',
             'description' => 'nullable|string',
-            'docker_images' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
+            'docker_images' => ['required', 'string', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/im'],
             'force_outgoing_ip' => 'sometimes|boolean',
             'file_denylist' => 'array',
             'startup' => 'required|string',
             'config_from' => 'sometimes|bail|nullable|numeric',
             'config_stop' => 'required_without:config_from|nullable|string|max:191',
diff --git a/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php b/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php
index 193f8676c..81505de15 100644
--- a/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php
+++ b/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php
@@ -7,10 +7,10 @@ use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
 class StoreNestFormRequest extends AdminFormRequest
 {
     public function rules(): array
     {
         return [
-            'name' => 'required|string|min:1|max:191',
+            'name' => 'required|string|min:1|max:191|regex:/^[\w\- ]+$/',
             'description' => 'string|nullable',
         ];
     }
 }
diff --git a/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php b/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php
index a104a91bc..231fec81b 100644
--- a/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php
+++ b/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php
@@ -22,9 +22,9 @@ class SetDockerImageRequest extends ClientApiRequest implements ClientPermission
         $server = $this->route()->parameter('server');
 
         Assert::isInstanceOf($server, Server::class);
 
         return [
-            'docker_image' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/', Rule::in(array_values($server->egg->docker_images))],
+            'docker_image' => ['required', 'string', 'max:191', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/', Rule::in(array_values($server->egg->docker_images))],
         ];
     }
 }
diff --git a/app/Models/Egg.php b/app/Models/Egg.php
index 40c2e3326..f0e668b27 100644
--- a/app/Models/Egg.php
+++ b/app/Models/Egg.php
@@ -121,11 +121,11 @@ class Egg extends Model
         'features' => 'array|nullable',
         'author' => 'required|string|email',
         'file_denylist' => 'array|nullable',
         'file_denylist.*' => 'string',
         'docker_images' => 'required|array|min:1',
-        'docker_images.*' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
+        'docker_images.*' => ['required', 'string', 'max:191', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/'],
         'startup' => 'required|nullable|string',
         'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
         'config_stop' => 'required_without:config_from|nullable|string|max:191',
         'config_startup' => 'required_without:config_from|nullable|json',
         'config_logs' => 'required_without:config_from|nullable|json',
diff --git a/app/Models/Server.php b/app/Models/Server.php
index ecc137f5d..f95e1deef 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -161,11 +161,11 @@ class Server extends Model
         'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
         'nest_id' => 'required|exists:nests,id',
         'egg_id' => 'required|exists:eggs,id',
         'startup' => 'required|string',
         'skip_scripts' => 'sometimes|boolean',
-        'image' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
+        'image' => ['required', 'string', 'max:191', 'regex:/^[\w\.\/\-:@ ]*$/'],
         'database_limit' => 'present|nullable|integer|min:0',
         'allocation_limit' => 'sometimes|nullable|integer|min:0',
         'backup_limit' => 'present|nullable|integer|min:0',
     ];
 
diff --git a/public/themes/pterodactyl/js/admin/new-server.js b/public/themes/pterodactyl/js/admin/new-server.js
index db138cbde..1fd80a921 100644
--- a/public/themes/pterodactyl/js/admin/new-server.js
+++ b/public/themes/pterodactyl/js/admin/new-server.js
@@ -86,11 +86,11 @@ $('#pEggId').on('change', function (event) {
     $('#pDefaultContainer').html('');
     const keys = Object.keys(images);
     for (let i = 0; i < keys.length; i++) {
         let opt = document.createElement('option');
         opt.value = images[keys[i]];
-        opt.innerHTML = keys[i] + " (" + images[keys[i]] + ")";
+        opt.innerText = keys[i] + " (" + images[keys[i]] + ")";
         $('#pDefaultContainer').append(opt);
     }
 
     if (!_.get(objectChain, 'startup', false)) {
         $('#pStartup').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!'));
diff --git a/resources/views/admin/servers/view/startup.blade.php b/resources/views/admin/servers/view/startup.blade.php
index 05330298e..c2d6dc11e 100644
--- a/resources/views/admin/servers/view/startup.blade.php
+++ b/resources/views/admin/servers/view/startup.blade.php
@@ -117,11 +117,11 @@
             $('#pDockerImage').html('');
             const keys = Object.keys(images);
             for (let i = 0; i < keys.length; i++) {
                 let opt = document.createElement('option');
                 opt.value = images[keys[i]];
-                opt.innerHTML = keys[i] + " (" + images[keys[i]] + ")";
+                opt.innerText = keys[i] + " (" + images[keys[i]] + ")";
                 if (objectChain.id === parseInt(Pterodactyl.server.egg_id) && Pterodactyl.server.image == opt.value) {
                     opt.selected = true
                 }
                 $('#pDockerImage').append(opt);
             }

Vulnerable code:             'variable' => $variable->name,
            'variable' => $variable->name,
        $this->alert->success(trans('admin/nests.notices.created', ['name' => $nest->name]))->flash();
        $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
            'docker_images' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
            'name' => 'required|string|min:1|max:191',
            'docker_image' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/', Rule::in(array_values($server->egg->docker_images))],
        'docker_images.*' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
        'image' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
        opt.innerHTML = keys[i] + " (" + images[keys[i]] + ")";
                opt.innerHTML = keys[i] + " (" + images[keys[i]] + ")";
Fixed code:             'variable' => htmlspecialchars($variable->name),
            'variable' => htmlspecialchars($variable->name),
        $this->alert->success(trans('admin/nests.notices.created', ['name' => htmlspecialchars($nest->name)]))->flash();
        $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => htmlspecialchars($request->input('ip'))]))
            'docker_images' => ['required', 'string', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/im'],
            'name' => 'required|string|min:1|max:191|regex:/^[\w\- ]+$/',
            'docker_image' => ['required', 'string', 'max:191', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/', Rule::in(array_values($server->egg->docker_images))],
        'docker_images.*' => ['required', 'string', 'max:191', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/'],
        'image' => ['required', 'string', 'max:191', 'regex:/^[\w\.\/\-:@ ]*$/'],
        opt.innerText = keys[i] + " (" + images[keys[i]] + ")";
                opt.innerText = keys[i] + " (" + images[keys[i]] + ")";