PYLOAD-NG · CVE-2026-123816 · Package
FIX → 0.5.0b3.dev101
pyLoad's API key cache is keyed only on the key ID, so for 300 seconds after a real key is used, any key with the same ID is accepted without checking the secret. An attacker who knows a active key ID, which are sequential integers, gets full API access as that user.
first public Oct 9, 2026 · 1 source
Could this affect your app?
Free first check from your website. Connect your repo for the full answer.
Any deployment of pyload-ng running a version below 0.5.0b3.dev101 with the web UI or API enabled is exposed, because normal API requests use the cache with its default 300-second lifetime. Deployments with no network exposure of the API, or that are already on the fixed version, can skip it.
Not affected if fixed-version, no-network-exposure
Who cares pyload-ng, python, self-hosted
pip show pyload-ng0.5.0b3.dev101Upgrade to 0.5.0b3.dev101
0.5.0b3.dev101 or laterThe API-key cache in check_apikey() allows an attacker to authenticate with a forged API key as long as the legitimate key for the same key_id has been used recently.
On a cache hit, the API key secret is never verified, resulting in a complete authentication bypass during the cache lifetime.
src/pyload/core/api/init.py
Api.check_apikey()src/pyload/core/database/apikey_database.py
check_apikey()_check_key()The cache is used by normal API requests through:
src/pyload/webui/app/helpers.pywhich calls:
Check it with your coding agent
It reads this page and tells you if your code is affected.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
api.check_apikey(api_key)
without providing a ttl, meaning the default 300-second cache is always enabled.
Successful API key validations are cached in:
self._apikey_cache
using the following structure:
{
key_id: (timestamp, cached_data)
}
The cache key is based only on key_id, and that value is parsed directly from the user-supplied API key rather than being obtained from a trusted lookup.
When a cache hit occurs within the TTL, check_apikey() returns:
{
"success": True,
"data": cached_data
}
after checking only:
expires_atAt no point on this execution path is the supplied secret:
apikey[-43:]
compared against the stored key hash.
The actual secret verification exists only in:
_check_key()
which correctly uses:
hmac.compare_digest()
However, this function is only reached on a cache miss through the database layer's check_apikey() implementation.
Furthermore, the cached object cannot be used for verification because it intentionally does not include the stored key hash.
The cached record contains only:
iduser_idnamecreated_atexpires_atlast_usedSince key_hash is absent, the cache lacks the information required to validate the presented secret.
As a result, once a legitimate request populates the cache, every subsequent request using the same key_id within the 300-second cache window is accepted without validating the secret.
key_id values are small sequential integers assigned when API keys are created (1, 2, 3, ...), making them trivial to enumerate.
The API key format is also publicly reconstructable from the source code.
An attacker therefore does not need to know any valid API key secret.
They only need a key_id whose legitimate key has been used within the previous five minutes, which is likely on an actively used deployment.
During that cache window, a forged API key follows exactly the same authentication path as a legitimate key and inherits the associated user's permissions.
This results in a complete remote authentication bypass.
I would classify the issue as Critical severity.
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
(AC:H only because successful exploitation depends on the cache already containing an entry.)
Example:
pl_11<43-character-secret>
where:
key_id = 1
curl \
-H "X-API-Key: pl_11<real-43-char-secret>" \
http://127.0.0.1:8000/api/status
Expected response:
HTTP/1.1 200 OK
curl \
-H "X-API-Key: pl_11xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
http://127.0.0.1:8000/api/status
Observed response:
HTTP/1.1 200 OK
The request succeeds even though the API key secret is entirely incorrect.
If the forged API key is sent before any legitimate request has populated the cache, the server correctly returns:
HTTP/1.1 401 Unauthorized
The only difference between the failing and succeeding forged request is whether the cache already contains an entry for that key_id.
This isolates the vulnerability specifically to the cache-hit path.
Cache hits should never replace secret verification.
Possible approaches include:
Store a value derived from the complete API key (or its hash) in the cache and verify it using hmac.compare_digest() on every cache hit before returning success.
Alternatively, key the cache using a value derived from the entire API key secret (for example, a cryptographic hash of the complete API key) rather than using only key_id.
Regardless of implementation, the security invariant should remain:
The API key secret must be validated on every authenticated request, regardless of whether the cache is hit or missed.
This page as .md · .json · CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.