Endless reprocessing/reupload of activity log data due to SQLite max parameters limit not being considered
Description
Wings is the server control plane for Pterodactyl, a free, open-source game server management panel. Starting in version 1.7.0 and prior to version 1.12.0, Wings does not consider SQLite max parameter limit when processing activity log entries allowing for low privileged user to trigger a condition that floods the panel with activity records. After Wings sends activity logs to the panel it deletes the processed activity entries from the wings SQLite database. However, it does not consider the max parameter limit of SQLite, 32766 as of SQLite 3.32.0. If wings attempts to delete more than 32766 entries from the SQLite database in one query, it triggers an error (SQL logic error: too many SQL variables (1)) and does not remove any entries from the database. These entries are then indefinitely re-processed and resent to the panel each time the cron runs. By successfully exploiting this vulnerability, an attacker can trigger a situation where wings will keep uploading the same activity data to the panel repeatedly (growing each time to include new activity) until the panels' database server runs out of disk space. Version 1.12.0 fixes the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/pterodactyl/wingsGo | >= 1.7.0, < 1.12.0 | 1.12.0 |
Affected products
1- Range: v1.11.0, v1.11.0-rc.1, v1.11.0-rc.2, …
Patches
109caa0d4995bMerge commit from fork
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
7- github.com/advisories/GHSA-2497-gp99-2m74ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-21696ghsaADVISORY
- github.com/pterodactyl/panel/commit/09caa0d4995bd924b53b9a9e9b4883ac27bd5607ghsaWEB
- github.com/pterodactyl/panel/releases/tag/v1.12.0ghsaWEB
- github.com/pterodactyl/wings/blob/9ffbcdcdb1163da823cf9959b9602df9f7dcb54a/internal/cron/activity_cron.goghsax_refsource_MISCWEB
- github.com/pterodactyl/wings/blob/9ffbcdcdb1163da823cf9959b9602df9f7dcb54a/internal/cron/sftp_cron.goghsax_refsource_MISCWEB
- github.com/pterodactyl/wings/security/advisories/GHSA-2497-gp99-2m74ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.