VYPR
Low severity2.3GHSA Advisory· Published May 26, 2026· Updated May 26, 2026

Pterodactyl has a database resource limit bypass via race condition in Client API

CVE-2026-35202

Description

Summary

The Pterodactyl Client API has a logic flaw that lets users bypass their assigned limits for database allocations. This happens because the database locking mechanism used in the controllers is totally broken and doesn't actually lock anything.

Details

Inside DatabaseController.php, the code tries to prevent multiple databases from being created at once by calling $server->databases()->lockForUpdate(). In Laravel, this just configures a query builder but never actually sends a command to the database because it’s missing a terminal method like count() or get(). It’s basically a no-op that does nothing.

Since there’s no real lock, multiple requests hitting the endpoint at the exact same time will all see that the database count is under the limit. They all move forward to the DeployServerDatabaseService and successfully create extra resources on the physical host.

Impact

Users are able to create more databases than they are supposed to, potentially also breaking the web interface.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Pterodactyl Client API has a race condition due to a missing terminal method on a query builder, allowing users to bypass database allocation limits.

Vulnerability

In DatabaseController.php, the code attempts to prevent concurrent database creation by calling $server->databases()->lockForUpdate(). However, in Laravel, this only configures the query builder and never actually executes a database query because a terminal method like get() or count() is missing. As a result, the lock is never applied, creating a race condition. The vulnerability affects the Pterodactyl Panel (specific versions not disclosed in available references [1], [2]).

Exploitation

An attacker needs only to be an authenticated user with permission to create databases on a server. By sending multiple concurrent requests to the database creation endpoint, each request sees the current database count as below the limit and proceeds to create a database, bypassing the intended limit.

Impact

Successful exploitation allows a user to create more databases than their assigned limit, potentially exhausting resources on the physical host and breaking the web interface due to unexpected database counts.

Mitigation

No fix has been disclosed in the available references [1], [2]. Users should monitor the Pterodactyl project for a patched version. Until a fix is released, no workaround is provided.

AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

2
a94b8bdb4e91

Fix API Key Limit Race Condition Bypass (#5620)

https://github.com/pterodactyl/panelNoah RossMay 23, 2026Fixed in 1.12.3via llm-release-walk
1 file changed · +10 7
  • app/Http/Controllers/Api/Client/ApiKeyController.php+10 7 modified
    @@ -5,6 +5,7 @@
     use Pterodactyl\Models\ApiKey;
     use Illuminate\Http\JsonResponse;
     use Pterodactyl\Facades\Activity;
    +use Illuminate\Support\Facades\DB;
     use Pterodactyl\Exceptions\DisplayException;
     use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
     use Pterodactyl\Transformers\Api\Client\ApiKeyTransformer;
    @@ -29,14 +30,16 @@ public function index(ClientApiRequest $request): array
          */
         public function store(StoreApiKeyRequest $request): array
         {
    -        if ($request->user()->apiKeys->count() >= 25) {
    -            throw new DisplayException('You have reached the account limit for number of API keys.');
    -        }
    +        $token = DB::transaction(function () use ($request) {
    +            if ($request->user()->apiKeys()->lockForUpdate()->count() >= 25) {
    +                throw new DisplayException('You have reached the account limit for number of API keys.');
    +            }
     
    -        $token = $request->user()->createToken(
    -            $request->input('description'),
    -            $request->input('allowed_ips')
    -        );
    +            return $request->user()->createToken(
    +                $request->input('description'),
    +                $request->input('allowed_ips')
    +            );
    +        });
     
             Activity::event('user:api-key.create')
                 ->subject($token->accessToken)
    
ec7231bd4aa2

Lock resources more explicitly when creating databases or backups (#5613)

https://github.com/pterodactyl/panelDane EverittApr 2, 2026Fixed in 1.12.3via llm-release-walk
4 files changed · +10 10
  • app/Http/Controllers/Api/Client/Servers/BackupController.php+1 1 modified
    @@ -78,7 +78,7 @@ public function store(StoreBackupRequest $request, Server $server): array
             }
     
             $backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
    -            $server->backups()->lockForUpdate();
    +            $server->backups()->lockForUpdate()->count();
     
                 $backup = $action->handle($server, $request->input('name'));
     
    
  • app/Http/Controllers/Api/Client/Servers/DatabaseController.php+5 6 modified
    @@ -6,6 +6,7 @@
     use Pterodactyl\Models\Server;
     use Pterodactyl\Models\Database;
     use Pterodactyl\Facades\Activity;
    +use Pterodactyl\Exceptions\DisplayException;
     use Pterodactyl\Services\Databases\DatabasePasswordService;
     use Pterodactyl\Transformers\Api\Client\DatabaseTransformer;
     use Pterodactyl\Services\Databases\DatabaseManagementService;
    @@ -49,7 +50,9 @@ public function index(GetDatabasesRequest $request, Server $server): array
         public function store(StoreDatabaseRequest $request, Server $server): array
         {
             $database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
    -            $server->databases()->lockForUpdate();
    +            if ($server->databases()->lockForUpdate()->count() >= $server->database_limit) {
    +                throw new DisplayException('Cannot create additional databases on this server: limit has been reached.');
    +            }
     
                 $database = $this->deployDatabaseService->handle($server, $request->validated());
     
    @@ -75,11 +78,7 @@ public function rotatePassword(RotatePasswordRequest $request, Server $server, D
             Activity::event('server:database.rotate-password')
                 ->subject($database)
                 ->property('name', $database->database)
    -            ->transaction(function () use ($database) {
    -                $database->lockForUpdate();
    -
    -                $this->passwordService->handle($database);
    -            });
    +            ->transaction(fn () => $this->passwordService->handle($database));
     
             return $this->fractal->item($database->refresh())
                 ->parseIncludes(['password'])
    
  • app/Services/Allocations/FindAssignableAllocationService.php+2 0 modified
    @@ -39,6 +39,7 @@ public function handle(Server $server): Allocation
             // server.
             /** @var Allocation|null $allocation */
             $allocation = $server->node->allocations()
    +            ->lockForUpdate()
                 ->where('ip', $server->allocation->ip)
                 ->whereNull('server_id')
                 ->inRandomOrder()
    @@ -102,6 +103,7 @@ protected function createNewAllocation(Server $server): Allocation
     
             /** @var Allocation $allocation */
             $allocation = $server->node->allocations()
    +            ->lockForUpdate()
                 ->where('ip', $server->allocation->ip)
                 ->where('port', $port)
                 ->firstOrFail();
    
  • app/Services/Databases/DatabasePasswordService.php+2 3 modified
    @@ -32,12 +32,11 @@ public function handle(Database|int $database): string
             $password = Utilities::randomStringWithSpecialCharacters(24);
     
             $this->connection->transaction(function () use ($database, $password) {
    -            $this->dynamic->set('dynamic', $database->database_host_id);
    -
    -            $this->repository->withoutFreshModel()->update($database->id, [
    +            $database->sharedLock()->update([
                     'password' => $this->encrypter->encrypt($password),
                 ]);
     
    +            $this->dynamic->set('dynamic', $database->database_host_id);
                 $this->repository->dropUser($database->username, $database->remote);
                 $this->repository->createUser($database->username, $database->remote, $password, $database->max_connections);
                 $this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
    

Vulnerability mechanics

Root cause

"Missing terminal method on a query builder call causes `lockForUpdate()` to be a no-op, so no database lock is ever acquired."

Attack vector

An attacker sends multiple concurrent HTTP requests to the database creation endpoint (`store()`) for the same server. Because `$server->databases()->lockForUpdate()` is a no-op (it only configures the query builder without executing it), no row-level lock is held [patch_id=2568918]. Each concurrent request independently reads the current database count, finds it under the limit, and proceeds to call `DeployServerDatabaseService`, resulting in more databases being created than the server's `database_limit` allows.

Affected code

The vulnerability is in `app/Http/Controllers/Api/Client/Servers/DatabaseController.php`, specifically in the `store()` method. The code calls `$server->databases()->lockForUpdate()` without a terminal method like `count()` or `get()`, so no actual database lock is acquired [patch_id=2568918]. The same broken locking pattern also existed in `BackupController.php` and `DatabasePasswordService.php` [patch_id=2568918].

What the fix does

The patch changes `$server->databases()->lockForUpdate()` to `$server->databases()->lockForUpdate()->count()` in `DatabaseController.php`, which actually executes the query and acquires a shared lock on the matching rows [patch_id=2568918]. It then checks whether the count meets or exceeds `$server->database_limit` and throws a `DisplayException` if so, preventing over-allocation. The same fix is applied to `BackupController.php` (`->count()` added) and `FindAssignableAllocationService.php` (proper `lockForUpdate()` calls added) [patch_id=2568918]. The `DatabasePasswordService.php` diff also moves the `sharedLock()` call earlier to ensure the password update is properly locked [patch_id=2568918].

Preconditions

  • authThe attacker must be an authenticated user with permission to create databases on a server they control.
  • configThe server must have a non-zero database_limit configured.
  • inputThe attacker must send multiple concurrent requests to the store endpoint.

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

References

2

News mentions

0

No linked articles in our index yet.