VYPR
High severityOSV Advisory· Published Jan 19, 2026· Updated Jan 20, 2026

Pterodactyl Wings's websocket endpoints have no visible rate limits or monitoring, allowing for DOS attacks under certain circumstances

CVE-2025-69199

Description

Wings is the server control plane for Pterodactyl, a free, open-source game server management panel. Prior to version 1.12.0, websockets within wings lack proper rate limiting and throttling. As a result a malicious user can open a large number of connections and then request data through these sockets, causing an excessive volume of data over the network and overloading the host system memory and cpu. Additionally, there is not a limit applied to the total size of messages being sent or received, allowing a malicious user to open thousands of websocket connections and then send massive volumes of information over the socket, overloading the host network, and causing increased CPU and memory load within Wings. Version 1.12.0 patches the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/pterodactyl/wingsGo
< 1.12.01.12.0

Affected products

1

Patches

1
09caa0d4995b

Merge commit from fork

https://github.com/pterodactyl/panelВсеволод МельникJan 6, 2026via ghsa
9 files changed · +128 32
  • app/Enum/ResourceLimit.php+65 0 added
    @@ -0,0 +1,65 @@
    +<?php
    +
    +namespace Pterodactyl\Enum;
    +
    +use Illuminate\Http\Request;
    +use Webmozart\Assert\Assert;
    +use Pterodactyl\Models\Server;
    +use Illuminate\Cache\RateLimiting\Limit;
    +use Illuminate\Support\Facades\RateLimiter;
    +use Illuminate\Routing\Middleware\ThrottleRequests;
    +
    +/**
    + * A basic resource throttler for individual servers. This is applied in addition
    + * to existing rate limits and allows the code to slow down speedy users that might
    + * be creating resources a little too quickly for comfort. This throttle generally
    + * only applies to creation flows, and not general view/edit/delete flows.
    + */
    +enum ResourceLimit
    +{
    +    case Allocation;
    +    case Backup;
    +    case Database;
    +    case Schedule;
    +    case Subuser;
    +    case Websocket;
    +    case FilePull;
    +
    +    public function throttleKey(): string
    +    {
    +        return mb_strtolower("api.client:server-resource:{$this->name}");
    +    }
    +
    +    /**
    +     * Returns a middleware that will throttle the specific resource by server. This
    +     * throttle applies to any user making changes to that resource on the specific
    +     * server, it is NOT per-user.
    +     */
    +    public function middleware(): string
    +    {
    +        return ThrottleRequests::using($this->throttleKey());
    +    }
    +
    +    public function limit(): Limit
    +    {
    +        return match($this) {
    +            self::Backup => Limit::perMinutes(15, 3),
    +            self::Database => Limit::perMinute(2),
    +            self::FilePull => Limit::perMinutes(10, 5),
    +            self::Subuser => Limit::perMinutes(15, 10),
    +            self::Websocket => Limit::perMinute(5),
    +            default => Limit::perMinute(2),
    +        };
    +    }
    +
    +    public static function boot(): void
    +    {
    +        foreach (self::cases() as $case) {
    +            RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
    +                Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
    +
    +                return $case->limit()->by($server->uuid);
    +            });
    +        }
    +    }
    +}
    
  • app/Http/Controllers/Api/Client/Servers/BackupController.php+12 6 modified
    @@ -74,15 +74,21 @@ public function store(StoreBackupRequest $request, Server $server): array
             // how best to allow a user to create a backup that is locked without also preventing
             // them from just filling up a server with backups that can never be deleted?
             if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
    -            $action->setIsLocked((bool) $request->input('is_locked'));
    +            $action->setIsLocked($request->boolean('is_locked'));
             }
     
    -        $backup = $action->handle($server, $request->input('name'));
    +        $backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
    +            $server->backups()->lockForUpdate();
     
    -        Activity::event('server:backup.start')
    -            ->subject($backup)
    -            ->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
    -            ->log();
    +            $backup = $action->handle($server, $request->input('name'));
    +
    +            $log->subject($backup)->property([
    +                'name' => $backup->name,
    +                'locked' => $request->boolean('is_locked'),
    +            ]);
    +
    +            return $backup;
    +        });
     
             return $this->fractal->item($backup)
                 ->transformWith($this->getTransformer(BackupTransformer::class))
    
  • app/Http/Controllers/Api/Client/Servers/DatabaseController.php+14 10 modified
    @@ -48,12 +48,15 @@ public function index(GetDatabasesRequest $request, Server $server): array
          */
         public function store(StoreDatabaseRequest $request, Server $server): array
         {
    -        $database = $this->deployDatabaseService->handle($server, $request->validated());
    +        $database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
    +            $server->databases()->lockForUpdate();
     
    -        Activity::event('server:database.create')
    -            ->subject($database)
    -            ->property('name', $database->database)
    -            ->log();
    +            $database = $this->deployDatabaseService->handle($server, $request->validated());
    +
    +            $log->subject($database)->property('name', $database->database);
    +
    +            return $database;
    +        });
     
             return $this->fractal->item($database)
                 ->parseIncludes(['password'])
    @@ -69,15 +72,16 @@ public function store(StoreDatabaseRequest $request, Server $server): array
          */
         public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
         {
    -        $this->passwordService->handle($database);
    -        $database->refresh();
    -
             Activity::event('server:database.rotate-password')
                 ->subject($database)
                 ->property('name', $database->database)
    -            ->log();
    +            ->transaction(function () use ($database) {
    +                $database->lockForUpdate();
     
    -        return $this->fractal->item($database)
    +                $this->passwordService->handle($database);
    +            });
    +
    +        return $this->fractal->item($database->refresh())
                 ->parseIncludes(['password'])
                 ->transformWith($this->getTransformer(DatabaseTransformer::class))
                 ->toArray();
    
  • app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php+11 8 modified
    @@ -6,6 +6,7 @@
     use Illuminate\Http\JsonResponse;
     use Pterodactyl\Facades\Activity;
     use Pterodactyl\Models\Allocation;
    +use Illuminate\Database\ConnectionInterface;
     use Pterodactyl\Exceptions\DisplayException;
     use Pterodactyl\Repositories\Eloquent\ServerRepository;
     use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
    @@ -23,6 +24,7 @@ class NetworkAllocationController extends ClientApiController
          * NetworkAllocationController constructor.
          */
         public function __construct(
    +        protected readonly ConnectionInterface $connection,
             private FindAssignableAllocationService $assignableAllocationService,
             private ServerRepository $serverRepository,
         ) {
    @@ -92,16 +94,17 @@ public function setPrimary(SetPrimaryAllocationRequest $request, Server $server,
          */
         public function store(NewAllocationRequest $request, Server $server): array
         {
    -        if ($server->allocations()->count() >= $server->allocation_limit) {
    -            throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
    -        }
    +        $allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) {
    +            if ($server->allocations()->lockForUpdate()->count() >= $server->allocation_limit) {
    +                throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
    +            }
     
    -        $allocation = $this->assignableAllocationService->handle($server);
    +            $allocation = $this->assignableAllocationService->handle($server);
     
    -        Activity::event('server:allocation.create')
    -            ->subject($allocation)
    -            ->property('allocation', $allocation->toString())
    -            ->log();
    +            $log->subject($allocation)->property('allocation', $allocation->toString());
    +
    +            return $allocation;
    +        });
     
             return $this->fractal->item($allocation)
                 ->transformWith($this->getTransformer(AllocationTransformer::class))
    
  • app/Http/Kernel.php+4 0 modified
    @@ -48,6 +48,10 @@ class Kernel extends HttpKernel
             ConvertEmptyStringsToNull::class,
         ];
     
    +    protected $middlewarePriority = [
    +        SubstituteClientBindings::class,
    +    ];
    +
         /**
          * The application's route middleware groups.
          */
    
  • app/Providers/RouteServiceProvider.php+3 0 modified
    @@ -4,6 +4,7 @@
     
     use Illuminate\Http\Request;
     use Pterodactyl\Models\Database;
    +use Pterodactyl\Enum\ResourceLimit;
     use Illuminate\Support\Facades\Route;
     use Illuminate\Cache\RateLimiting\Limit;
     use Illuminate\Support\Facades\RateLimiter;
    @@ -106,5 +107,7 @@ protected function configureRateLimiting(): void
                     config('http.rate_limit.application')
                 )->by($key);
             });
    +
    +        ResourceLimit::boot();
         }
     }
    
  • app/Services/Activity/ActivityLogService.php+2 0 modified
    @@ -166,6 +166,8 @@ public function clone(): self
          * and will only save the activity log entry if everything else successfully
          * settles.
          *
    +     * @param \Closure($this): mixed $callback
    +     *
          * @throws \Throwable
          */
         public function transaction(\Closure $callback)
    
  • config/http.php+1 1 modified
    @@ -13,7 +13,7 @@
         */
         'rate_limit' => [
             'client_period' => 1,
    -        'client' => env('APP_API_CLIENT_RATELIMIT', 720),
    +        'client' => env('APP_API_CLIENT_RATELIMIT', 128),
     
             'application_period' => 1,
             'application' => env('APP_API_APPLICATION_RATELIMIT', 240),
    
  • routes/api-client.php+16 7 modified
    @@ -1,5 +1,6 @@
     <?php
     
    +use Pterodactyl\Enum\ResourceLimit;
     use Illuminate\Support\Facades\Route;
     use Pterodactyl\Http\Controllers\Api\Client;
     use Pterodactyl\Http\Middleware\Activity\ServerSubject;
    @@ -60,7 +61,9 @@
         ],
     ], function () {
         Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view');
    -    Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws');
    +    Route::middleware([ResourceLimit::Websocket->middleware()])
    +        ->get('/websocket', Client\Servers\WebsocketController::class)
    +        ->name('api:client:server.ws');
         Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources');
         Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity');
     
    @@ -69,7 +72,8 @@
     
         Route::group(['prefix' => '/databases'], function () {
             Route::get('/', [Client\Servers\DatabaseController::class, 'index']);
    -        Route::post('/', [Client\Servers\DatabaseController::class, 'store']);
    +        Route::middleware([ResourceLimit::Database->middleware()])
    +            ->post('/', [Client\Servers\DatabaseController::class, 'store']);
             Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']);
             Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']);
         });
    @@ -86,13 +90,15 @@
             Route::post('/delete', [Client\Servers\FileController::class, 'delete']);
             Route::post('/create-folder', [Client\Servers\FileController::class, 'create']);
             Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']);
    -        Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']);
    +        Route::middleware([ResourceLimit::FilePull->middleware()])
    +            ->post('/pull', [Client\Servers\FileController::class, 'pull']);
             Route::get('/upload', Client\Servers\FileUploadController::class);
         });
     
         Route::group(['prefix' => '/schedules'], function () {
             Route::get('/', [Client\Servers\ScheduleController::class, 'index']);
    -        Route::post('/', [Client\Servers\ScheduleController::class, 'store']);
    +        Route::middleware([ResourceLimit::Schedule->middleware()])
    +            ->post('/', [Client\Servers\ScheduleController::class, 'store']);
             Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']);
             Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']);
             Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']);
    @@ -105,15 +111,17 @@
     
         Route::group(['prefix' => '/network'], function () {
             Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']);
    -        Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
    +        Route::middleware([ResourceLimit::Allocation->middleware()])
    +            ->post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
             Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']);
             Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']);
             Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']);
         });
     
         Route::group(['prefix' => '/users'], function () {
             Route::get('/', [Client\Servers\SubuserController::class, 'index']);
    -        Route::post('/', [Client\Servers\SubuserController::class, 'store']);
    +        Route::middleware([ResourceLimit::Subuser->middleware()])
    +            ->post('/', [Client\Servers\SubuserController::class, 'store']);
             Route::get('/{user}', [Client\Servers\SubuserController::class, 'view']);
             Route::post('/{user}', [Client\Servers\SubuserController::class, 'update']);
             Route::delete('/{user}', [Client\Servers\SubuserController::class, 'delete']);
    @@ -125,7 +133,8 @@
             Route::get('/{backup}', [Client\Servers\BackupController::class, 'view']);
             Route::get('/{backup}/download', [Client\Servers\BackupController::class, 'download']);
             Route::post('/{backup}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
    -        Route::post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
    +        Route::middleware([ResourceLimit::Backup->middleware()])
    +            ->post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
             Route::delete('/{backup}', [Client\Servers\BackupController::class, 'delete']);
         });
     
    

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

5

News mentions

0

No linked articles in our index yet.