Skip to content

Commit a94b8bd

Browse files
authored
Fix API Key Limit Race Condition Bypass (#5620)
The API key creation endpoint checks that a user has fewer than 25 keys before creating a new one. The problem is that the count was read from an eager-loaded collection (`$user->apiKeys->count()`) with no lock held, so concurrent requests could both pass the check and each create a key, pushing the user past the 25-key cap. The fix wraps the count check and key creation in a single database transaction with `lockForUpdate()` on the query. Only one request at a time can evaluate and modify the count, closing the race window. ### Proof of Concept Run this in the browser console while authenticated with a user that has 24 API keys: ```js (async () => { const makeKey = (desc) => fetch('/api/client/account/api-keys', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)[1]), }, body: JSON.stringify({ description: desc, allowed_ips: [] }), }); const [r1, r2] = await Promise.all([makeKey('0024'), makeKey('0025')]); console.log('0024:', r1.status, (await r1.text()).slice(0, 200)); console.log('0025:', r2.status, (await r2.text()).slice(0, 200)); })(); ``` On the old code, both requests can return 200 (you may need to run this a few times to hit the race window). After the fix, the second request correctly returns a 400 error.
1 parent 7ffcd63 commit a94b8bd

1 file changed

Lines changed: 10 additions & 7 deletions

File tree

app/Http/Controllers/Api/Client/ApiKeyController.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Pterodactyl\Models\ApiKey;
66
use Illuminate\Http\JsonResponse;
77
use Pterodactyl\Facades\Activity;
8+
use Illuminate\Support\Facades\DB;
89
use Pterodactyl\Exceptions\DisplayException;
910
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
1011
use Pterodactyl\Transformers\Api\Client\ApiKeyTransformer;
@@ -29,14 +30,16 @@ public function index(ClientApiRequest $request): array
2930
*/
3031
public function store(StoreApiKeyRequest $request): array
3132
{
32-
if ($request->user()->apiKeys->count() >= 25) {
33-
throw new DisplayException('You have reached the account limit for number of API keys.');
34-
}
33+
$token = DB::transaction(function () use ($request) {
34+
if ($request->user()->apiKeys()->lockForUpdate()->count() >= 25) {
35+
throw new DisplayException('You have reached the account limit for number of API keys.');
36+
}
3537

36-
$token = $request->user()->createToken(
37-
$request->input('description'),
38-
$request->input('allowed_ips')
39-
);
38+
return $request->user()->createToken(
39+
$request->input('description'),
40+
$request->input('allowed_ips')
41+
);
42+
});
4043

4144
Activity::event('user:api-key.create')
4245
->subject($token->accessToken)

0 commit comments

Comments
 (0)