diff --git a/CHANGELOG.md b/CHANGELOG.md index a6980d0d8b..3170fc963d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## v1.11.6 + +### Changed + +* Better node ownership checks for internal backup endpoints +* Improved validation rules on `docker_image` fields to prevent invalid inputs + +### Fixed + +* Multiple XSS vulnerabilities in the admin area ([GHSA-384w-wffr-x63q](https://github.com/pterodactyl/panel/security/advisories/GHSA-384w-wffr-x63q)) + ## v1.11.5 ### Fixed * Rust egg using the wrong Docker image, breaking Rust modding frameworks. diff --git a/SECURITY.md b/SECURITY.md index 087b67fc14..250790fc9b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,6 @@ | 面板 | 守护进程 | 支持状况 | |--------|--------------| ------------------ | -| 1.10.x | wings@1.7.x | :white_check_mark: | | 1.11.x | wings@1.11.x | :white_check_mark: | | 0.7.x | daemon@0.6.x | :x: | diff --git a/app/Http/Controllers/Admin/Nests/EggVariableController.php b/app/Http/Controllers/Admin/Nests/EggVariableController.php index 40274b3239..2b84e8b022 100644 --- a/app/Http/Controllers/Admin/Nests/EggVariableController.php +++ b/app/Http/Controllers/Admin/Nests/EggVariableController.php @@ -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); diff --git a/app/Http/Controllers/Admin/Nests/NestController.php b/app/Http/Controllers/Admin/Nests/NestController.php index 037dd09430..311a5df28b 100644 --- a/app/Http/Controllers/Admin/Nests/NestController.php +++ b/app/Http/Controllers/Admin/Nests/NestController.php @@ -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); } diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 573a1d9f8d..71094a5175 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -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); diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php b/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php index 7d92e0b1aa..15fe8d9bd1 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php @@ -32,18 +32,32 @@ public function __construct(private BackupManager $backupManager) */ public function __invoke(Request $request, string $backup): JsonResponse { + // Get the node associated with the request. + /** @var \Pterodactyl\Models\Node $node */ + $node = $request->attributes->get('node'); + // Get the size query parameter. $size = (int) $request->query('size'); if (empty($size)) { throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.'); } - /** @var \Pterodactyl\Models\Backup $backup */ - $backup = Backup::query()->where('uuid', $backup)->firstOrFail(); + /** @var \Pterodactyl\Models\Backup $model */ + $model = Backup::query() + ->where('uuid', $backup) + ->firstOrFail(); + + // Check that the backup is "owned" by the node making the request. This avoids other nodes + // from messing with backups that they don't own. + /** @var \Pterodactyl\Models\Server $server */ + $server = $model->server; + if ($server->node_id !== $node->id) { + throw new HttpForbiddenException('You do not have permission to access that backup.'); + } // Prevent backups that have already been completed from trying to // be uploaded again. - if (!is_null($backup->completed_at)) { + if (!is_null($model->completed_at)) { throw new ConflictHttpException('This backup is already in a completed state.'); } @@ -54,7 +68,7 @@ public function __invoke(Request $request, string $backup): JsonResponse } // The path where backup will be uploaded to - $path = sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid); + $path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid); // Get the S3 client $client = $adapter->getClient(); @@ -92,7 +106,7 @@ public function __invoke(Request $request, string $backup): JsonResponse } // Set the upload_id on the backup in the database. - $backup->update(['upload_id' => $params['UploadId']]); + $model->update(['upload_id' => $params['UploadId']]); return new JsonResponse([ 'parts' => $parts, diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php index f9c2a7932e..7b30e07582 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php @@ -30,8 +30,22 @@ public function __construct(private BackupManager $backupManager) */ public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse { + // Get the node associated with the request. + /** @var \Pterodactyl\Models\Node $node */ + $node = $request->attributes->get('node'); + /** @var \Pterodactyl\Models\Backup $model */ - $model = Backup::query()->where('uuid', $backup)->firstOrFail(); + $model = Backup::query() + ->where('uuid', $backup) + ->firstOrFail(); + + // Check that the backup is "owned" by the node making the request. This avoids other nodes + // from messing with backups that they don't own. + /** @var \Pterodactyl\Models\Server $server */ + $server = $model->server; + if ($server->node_id !== $node->id) { + throw new HttpForbiddenException('You do not have permission to access that backup.'); + } if ($model->is_successful) { throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.'); diff --git a/app/Http/Requests/Admin/Egg/EggFormRequest.php b/app/Http/Requests/Admin/Egg/EggFormRequest.php index 0f153393fb..0a88f5a74c 100644 --- a/app/Http/Requests/Admin/Egg/EggFormRequest.php +++ b/app/Http/Requests/Admin/Egg/EggFormRequest.php @@ -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', 'regex:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/im'], 'force_outgoing_ip' => 'sometimes|boolean', 'file_denylist' => 'array', 'startup' => 'required|string', diff --git a/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php b/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php index 193f8676c8..81505de154 100644 --- a/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php +++ b/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php @@ -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', ]; } diff --git a/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php b/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php index f618de3705..231fec81b9 100644 --- a/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php @@ -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:/^[\w#\.\/\- ]*\|*[\w\.\/\-:@ ]*$/', Rule::in(array_values($server->egg->docker_images))], ]; } } diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 31c3e34200..f0e668b27e 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -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:/^[\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', diff --git a/app/Models/Server.php b/app/Models/Server.php index d53af3038c..f95e1deefc 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -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:/^[\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 4288294836..61d8ed22f8 100644 --- a/public/themes/pterodactyl/js/admin/new-server.js +++ b/public/themes/pterodactyl/js/admin/new-server.js @@ -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); } @@ -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) ? '必需 ' : ''; let dataAppend = ' \
\ - \ - \ -

' + item.description + '
\ - 启动时访问: {{' + item.env_variable + '}}
\ - 验证规则: ' + item.rules + '

\ + \ + \ +

' + escapeHtml(item.description) + '
\ + 启动时访问: {{' + escapeHtml(item.env_variable) + '}}
\ + 验证规则: ' + escapeHtml(item.rules) + '

\
\ '; $('#appendVariablesTo').append(dataAppend); diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index f2bea86d0f..602ac217d3 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -40,6 +40,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({ 'application/x-xz', // .tar.xz, .xz 'application/zstd', // .tar.zst, .zst 'application/zip', // .zip + 'application/x-7z-compressed', // .7z ].indexOf(this.mimetype) >= 0 ); }, diff --git a/resources/views/admin/nodes/view/index.blade.php b/resources/views/admin/nodes/view/index.blade.php index 03d7ab56b0..cdc462168a 100644 --- a/resources/views/admin/nodes/view/index.blade.php +++ b/resources/views/admin/nodes/view/index.blade.php @@ -145,14 +145,20 @@ @section('footer-scripts') @parent