CVE-2024-34067 (published 2024-05-03T18:15Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/pterodactyl/panel
commit_details: repo_url: https://github.com/pterodactyl/panel
author: Matthew Penner
author_date: 2024-04-11 10:47:00-06:00
msg: ui(admin): better handling of manual HTML rendering
repository: repo_url: https://github.com/pterodactyl/panel
repo_name: pterodactyl/panel
description: Pterodactyl® is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.
repo_language: PHP
owner: pterodactyl
file_changes: filename: new-server.js
change_type: ModificationType.MODIFY
diff: @@ -109,6 +109,12 @@ $('#pEggId').on('change', function (event) {
         ),
     });
 
+    function escapeHtml(str) {
+        var div = document.createElement('div');
+        div.appendChild(document.createTextNode(str));
+        return div.innerHTML;
+    }
+
     const variableIds = {};
     $('#appendVariablesTo').html('');
     $.each(_.get(objectChain, 'variables', []), function (i, item) {
@@ -117,11 +123,11 @@ $('#pEggId').on('change', function (event) {
         let isRequired = (item.required === 1) ? '<span class="label label-danger">Required</span> ' : '';
         let dataAppend = ' \
             <div class="form-group col-sm-6"> \
-                <label for="var_ref_' + item.id + '" class="control-label">' + isRequired + item.name + '</label> \
-                <input type="text" id="var_ref_' + item.id + '" autocomplete="off" name="environment[' + item.env_variable + ']" class="form-control" value="' + item.default_value + '" /> \
-                <p class="text-muted small">' + item.description + '<br /> \
-                <strong>Access in Startup:</strong> <code>{{' + item.env_variable + '}}</code><br /> \
-                <strong>Validation Rules:</strong> <code>' + item.rules + '</code></small></p> \
+                <label for="var_ref_' + escapeHtml(item.id) + '" class="control-label">' + isRequired + escapeHtml(item.name) + '</label> \
+                <input type="text" id="var_ref_' + escapeHtml(item.id) + '" autocomplete="off" name="environment[' + escapeHtml(item.env_variable) + ']" class="form-control" value="' + escapeHtml(item.default_value) + '" /> \
+                <p class="text-muted small">' + escapeHtml(item.description) + '<br /> \
+                <strong>Access in Startup:</strong> <code>{{' + escapeHtml(item.env_variable) + '}}</code><br /> \
+                <strong>Validation Rules:</strong> <code>' + escapeHtml(item.rules) + '</code></small></p> \
             </div> \
         ';
         $('#appendVariablesTo').append(dataAppend);

programming_language: JavaScript
filename: index.blade.php
change_type: ModificationType.MODIFY
diff: @@ -145,14 +145,20 @@
 @section('footer-scripts')
     @parent
     <script>
+    function escapeHtml(str) {
+        var div = document.createElement('div');
+        div.appendChild(document.createTextNode(str));
+        return div.innerHTML;
+    }
+
     (function getInformation() {
         $.ajax({
             method: 'GET',
             url: '/admin/nodes/view/{{ $node->id }}/system-information',
             timeout: 5000,
         }).done(function (data) {
-            $('[data-attr="info-version"]').html(data.version);
-            $('[data-attr="info-system"]').html(data.system.type + ' (' + data.system.arch + ') <code>' + data.system.release + '</code>');
+            $('[data-attr="info-version"]').html(escapeHtml(data.version));
+            $('[data-attr="info-system"]').html(escapeHtml(data.system.type) + ' (' + escapeHtml(data.system.arch) + ') <code>' + escapeHtml(data.system.release) + '</code>');
             $('[data-attr="info-cpus"]').html(data.system.cpus);
         }).fail(function (jqXHR) {
 

programming_language: PHP
filename: startup.blade.php
change_type: ModificationType.MODIFY
diff: @@ -107,6 +107,12 @@
     @parent
     {!! Theme::js('vendor/lodash/lodash.js') !!}
     <script>
+    function escapeHtml(str) {
+        var div = document.createElement('div');
+        div.appendChild(document.createTextNode(str));
+        return div.innerHTML;
+    }
+
     $(document).ready(function () {
         $('#pEggId').select2({placeholder: 'Select a Nest Egg'}).on('change', function () {
             var selectedEgg = _.isNull($(this).val()) ? $(this).find('option').first().val() : $(this).val();
@@ -149,15 +155,15 @@
                     <div class="col-xs-12"> \
                         <div class="box"> \
                             <div class="box-header with-border"> \
-                                <h3 class="box-title">' + isRequired + item.name + '</h3> \
+                                <h3 class="box-title">' + isRequired + escapeHtml(item.name) + '</h3> \
                             </div> \
                             <div class="box-body"> \
-                                <input name="environment[' + item.env_variable + ']" class="form-control" type="text" id="egg_variable_' + item.env_variable + '" /> \
-                                <p class="no-margin small text-muted">' + item.description + '</p> \
+                                <input name="environment[' + escapeHtml(item.env_variable) + ']" class="form-control" type="text" id="egg_variable_' + escapeHtml(item.env_variable) + '" /> \
+                                <p class="no-margin small text-muted">' + escapeHtml(item.description) + '</p> \
                             </div> \
                             <div class="box-footer"> \
-                                <p class="no-margin text-muted small"><strong>Startup Command Variable:</strong> <code>' + item.env_variable + '</code></p> \
-                                <p class="no-margin text-muted small"><strong>Input Rules:</strong> <code>' + item.rules + '</code></p> \
+                                <p class="no-margin text-muted small"><strong>Startup Command Variable:</strong> <code>' + escapeHtml(item.env_variable) + '</code></p> \
+                                <p class="no-margin text-muted small"><strong>Input Rules:</strong> <code>' + escapeHtml(item.rules) + '</code></p> \
                             </div> \
                         </div> \
                     </div>';

programming_language: HTML
repo_url: https://github.com/pterodactyl/panel
commit_details: repo_url: https://github.com/pterodactyl/panel
author: Matthew Penner
author_date: 2024-04-10 14:41:32-06:00
msg: app: improve `docker_image` validation
repository: repo_url: https://github.com/pterodactyl/panel
repo_name: pterodactyl/panel
description: Pterodactyl® is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.
repo_language: PHP
owner: pterodactyl
file_changes: filename: EggFormRequest.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ class EggFormRequest extends AdminFormRequest
         $rules = [
             'name' => 'required|string|max:191',
             'description' => 'nullable|string',
-            'docker_images' => 'required|string',
+            'docker_images' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
             'force_outgoing_ip' => 'sometimes|boolean',
             'file_denylist' => 'array',
             'startup' => 'required|string',

programming_language: PHP
filename: SetDockerImageRequest.php
change_type: ModificationType.MODIFY
diff: @@ -24,7 +24,7 @@ class SetDockerImageRequest extends ClientApiRequest implements ClientPermission
         Assert::isInstanceOf($server, Server::class);
 
         return [
-            'docker_image' => ['required', 'string', Rule::in(array_values($server->egg->docker_images))],
+            'docker_image' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/', Rule::in(array_values($server->egg->docker_images))],
         ];
     }
 }

programming_language: PHP
filename: Egg.php
change_type: ModificationType.MODIFY
diff: @@ -123,7 +123,7 @@ class Egg extends Model
         'file_denylist' => 'array|nullable',
         'file_denylist.*' => 'string',
         'docker_images' => 'required|array|min:1',
-        'docker_images.*' => 'required|string',
+        'docker_images.*' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
         'startup' => 'required|nullable|string',
         'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
         'config_stop' => 'required_without:config_from|nullable|string|max:191',

programming_language: PHP
filename: Server.php
change_type: ModificationType.MODIFY
diff: @@ -163,7 +163,7 @@ class Server extends Model
         'egg_id' => 'required|exists:eggs,id',
         'startup' => 'required|string',
         'skip_scripts' => 'sometimes|boolean',
-        'image' => 'required|string|max:191',
+        'image' => ['required', 'string', 'max:191', 'regex:/^([a-zA-Z0-9 .#_\/\-]*)(\|*)([a-zA-Z0-9 .\/:@]*)$/'],
         'database_limit' => 'present|nullable|integer|min:0',
         'allocation_limit' => 'sometimes|nullable|integer|min:0',
         'backup_limit' => 'present|nullable|integer|min:0',

programming_language: PHP
repo_url: https://github.com/pterodactyl/panel
commit_details: repo_url: https://github.com/pterodactyl/panel
author: Matthew Penner
author_date: 2024-04-10 17:39:26-06:00
msg: admin: tweaks to validation and rendering
repository: repo_url: https://github.com/pterodactyl/panel
repo_name: pterodactyl/panel
description: Pterodactyl® is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.
repo_language: PHP
owner: pterodactyl
file_changes: filename: EggVariableController.php
change_type: ModificationType.MODIFY
diff: @@ -69,7 +69,7 @@ class EggVariableController extends Controller
     {
         $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);
@@ -82,7 +82,7 @@ class EggVariableController extends Controller
     {
         $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);

programming_language: PHP
filename: NestController.php
change_type: ModificationType.MODIFY
diff: @@ -56,7 +56,7 @@ class NestController extends Controller
     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);
     }

programming_language: PHP
filename: NodesController.php
change_type: ModificationType.MODIFY
diff: @@ -131,7 +131,7 @@ class NodesController extends Controller
             ['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);

programming_language: PHP
filename: EggFormRequest.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ class EggFormRequest extends AdminFormRequest
         $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',

programming_language: PHP
filename: StoreNestFormRequest.php
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ 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',
         ];
     }

programming_language: PHP
filename: SetDockerImageRequest.php
change_type: ModificationType.MODIFY
diff: @@ -24,7 +24,7 @@ class SetDockerImageRequest extends ClientApiRequest implements ClientPermission
         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))],
         ];
     }
 }

programming_language: PHP
filename: Egg.php
change_type: ModificationType.MODIFY
diff: @@ -123,7 +123,7 @@ class Egg extends Model
         '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',

programming_language: PHP
filename: Server.php
change_type: ModificationType.MODIFY
diff: @@ -163,7 +163,7 @@ class Server extends Model
         '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',

programming_language: PHP
filename: new-server.js
change_type: ModificationType.MODIFY
diff: @@ -88,7 +88,7 @@ $('#pEggId').on('change', function (event) {
     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);
     }
 

programming_language: JavaScript
filename: startup.blade.php
change_type: ModificationType.MODIFY
diff: @@ -119,7 +119,7 @@
             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
                 }

programming_language: PHP