External API key validation: per-request scrypt with no rate limiting
⚠️ Search for existing issues first ⚠️
- I have searched the existing issues, and there is no existing issue for my problem
Which Operating System are you using?
Docker
Which version of AutoGPT are you using?
Master (branch)
What LLM Provider do you use?
Other (detail in issue)
Which area covers your issue best?
Other
What commit or version are you using?
c035ab1f91b8556f86e87be26dcaf31677f64ba5
Describe your issue.
External API keys are agpt_ + 43 random characters; the first 8 — i.e. agpt_ + 3 random chars, the head — are stored in plaintext and used to look up candidate keys in the DB (get_active_api_keys_by_head), and the full key is then verified with scrypt (n=2^14, r=8, p=1, ~16 MB working memory per verification). Consequently, every request whose head matches a stored key runs a full scrypt verification per matched candidate — even when the rest of the key is garbage and the request is guaranteed to fail — and nothing on this path is rate-limited. An attacker only needs a head that matches some stored key (e.g. the head of their own free account's key), and with only 64³ = 262,144 possible heads the whole head space is enumerable even without owning any key; the server can be made to burn CPU and memory at request rate, starving the shared external API for all users.
Expected behavior
Authentication failure is cheap and bounded — an invalid key is rejected quickly without expensive crypto, and abusive request rates are throttled (429, per IP or per key), so an unauthenticated caller cannot drive up the per-request server cost.
Actual behavior (measured, in-container against /external-api/v1/integrations/providers)
| # | Test (20 serial unless noted) | What it shows | Avg latency/req | Status |
|---|---|---|---|---|
| 1 | Valid API key | the auth path itself: full scrypt runs on every request (the 403 is the permission layer rejecting this key's scopes, i.e. after auth) | 35 ms | 403 |
| 2 | Valid head + garbage rest-of-key | the attack: the head matches a stored key, so full scrypt runs although the request is guaranteed to fail | 30 ms | 401 |
| 3 | Unknown head (agpt_zzz…) — control |
no DB head match → no scrypt, cheap rejection | 8 ms | 401 |
| 4 | 200 concurrent requests, valid head + garbage rest | no rate limiting anywhere on the path | — | 200 × 401, 0 × 429 |
The ~4× latency gap between rows 2 and 3 is the per-request scrypt cost, paid on every head-matched request regardless of outcome, and row 4 shows it can be driven at concurrent request rate. The attacker needs no one else's key: registering a free account already reveals the head of one's own key.
Upload Activity Log Content
No response
Upload Error Log Content
No response
Source: Significant-Gravitas/AutoGPT