VYPR
Moderate severityNVD Advisory· Published May 3, 2024· Updated Aug 2, 2024

Multiple cross site scripting (XSS) vulnerabilities in the admin area of Pterodactyl panel

CVE-2024-34067

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pterodactyl/panelPackagist
< 1.11.61.11.6

Affected products

1

Patches

3
0dad4c5a4886

ui(admin): better handling of manual HTML rendering

https://github.com/pterodactyl/panelMatthew PennerApr 11, 2024via ghsa
3 files changed · +30 12
  • public/themes/pterodactyl/js/admin/new-server.js+11 5 modified
    @@ -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);
    
  • resources/views/admin/nodes/view/index.blade.php+8 2 modified
    @@ -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) {
     
    
  • resources/views/admin/servers/view/startup.blade.php+11 5 modified
    @@ -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>';
    
f671046947e4

admin: tweaks to validation and rendering

https://github.com/pterodactyl/panelMatthew PennerApr 10, 2024via ghsa
10 files changed · +11 11
  • app/Http/Controllers/Admin/Nests/EggVariableController.php+2 2 modified
    @@ -69,7 +69,7 @@ public function update(EggVariableFormRequest $request, Egg $egg, EggVariable $v
         {
             $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 @@ 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);
    
  • app/Http/Controllers/Admin/Nests/NestController.php+1 1 modified
    @@ -56,7 +56,7 @@ public function create(): View
         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);
         }
    
  • app/Http/Controllers/Admin/NodesController.php+1 1 modified
    @@ -131,7 +131,7 @@ public function allocationRemoveBlock(Request $request, int $node): RedirectResp
                 ['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);
    
  • app/Http/Requests/Admin/Egg/EggFormRequest.php+1 1 modified
    @@ -11,7 +11,7 @@ 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',
    
  • app/Http/Requests/Admin/Nest/StoreNestFormRequest.php+1 1 modified
    @@ -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',
             ];
         }
    
  • app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php+1 1 modified
    @@ -24,7 +24,7 @@ public function rules(): array
             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))],
             ];
         }
     }
    
  • app/Models/Egg.php+1 1 modified
    @@ -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',
    
  • app/Models/Server.php+1 1 modified
    @@ -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',
    
  • public/themes/pterodactyl/js/admin/new-server.js+1 1 modified
    @@ -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);
         }
     
    
  • resources/views/admin/servers/view/startup.blade.php+1 1 modified
    @@ -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
                     }
    
1172d71d3156

app: improve `docker_image` validation

https://github.com/pterodactyl/panelMatthew PennerApr 10, 2024via ghsa
4 files changed · +4 4
  • app/Http/Requests/Admin/Egg/EggFormRequest.php+1 1 modified
    @@ -11,7 +11,7 @@ public function rules(): array
             $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',
    
  • app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php+1 1 modified
    @@ -24,7 +24,7 @@ public function rules(): array
             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))],
             ];
         }
     }
    
  • app/Models/Egg.php+1 1 modified
    @@ -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',
    
  • app/Models/Server.php+1 1 modified
    @@ -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',
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.