GHSA-889w-m37p-88m5
pyLoad: Api.set_user_permission never invalidates the target's session
Summary
### Summary An admin who revokes or downgrades a non-admin user's permissions through pyLoad's own documented `/api/ ` RPC surface — e.g. `POST /api/set_user_permission` (or its legacy alias `/api/setUserPermission`) — updates the target's database row but never invalidates that user's existing Flask cookie session. The demoted user keeps their pre-revocation `role`/`permission` bits on every subsequent WebUI page load and every subsequent `/api/ ` call made with that cookie, for up to the default `session_lifetime` of ~31 days, or until they voluntarily log out. No action by the demoted user is required beyond already being logged in at the time of revocation. ### Details The vulnerable method, verbatim, at `src/pyload/core/api/__init__.py:1652-1656`: ``` @legacy("setUserPermission") @post def set_user_permission(self, user: str, permission: int, role: int) -> None: self.pyload.db.set_permission(user, permission) self.pyload.db.set_role(user, role) ``` It carries no `@permission(...)` decorator and its body never calls `clear_all_user_sessions` or any other session-invalidation routine. The **only** call site in the entire codebase that pairs a permission/role change with `clear_all_user_sessions` is the WebUI's `update_users()` route, `src/pyload/webui/app/blueprints/json_blueprint.py:442-444`: ``` api.set_user_permission(name, data["permission"], data["role"]) if was_changed: clear_all_user_sessions(name) ``` A repo-wide grep for `set_user_permission|setUserPermission` returns exactly 3 hits: the `@legacy` alias registration (`core/api/__init__.py:1652`), the method definition itself (`:1654`), and this one call site (`json_blueprint.py:442`). No other caller exists. pyLoad's own public RPC dispatcher reaches `Api.set_user_permission` directly, bypassing `json_blueprint.py` entirely. `src/pyload/webui/app/blueprints/api_blueprint.py:21-26` registers `rpc(func, args="")` on `/api/ ` and `/api/ / ` for `GET`/`POST`; line 68 dispatches with `response = jsonify(getattr(api, func)(**json_request_body))` — i.e. it invokes any method on the `Api` object by name, including `set_user_permission` (also reachable via form/multipart bodies at `:72`/`:77`). The only authorization gate before that dispatch, at `api_blueprint.py:50`, is: ``` if not api.is_authorized(func, {"role": user_info["role"], "permission": user_info["permission"]}): ``` `is_authorized` (`core/api/__init__.py:1419-1432`) returns `True` outright when the **caller's** role is `Role.ADMIN`; otherwise it requires `func_name in perm_map`. `set_user_permission` has no `@permission` decorator, so it is absent from `perm_map` (the module comment at `core/api/__init__.py:37` states "unlisted functions are for admins only"). This gate concerns only the caller's own authorization — it says nothing about, and never touches, the **target** user's existing session. ### The stale session survives The demoted user's session was populated once, at login, and is never refreshed: - `helpers.py:170,179,183-185` — `parse_permissions(session)` derives the caller's effective admin/permission bits from `session.get("role")` and `session.get("perms")`. - These values are written once by `set_session()` (`helpers.py:223-234`), called only from `app_blueprint.py:88` (login) and `:100` (autologin). A repo-wide grep for `before_request` inside `src/pyload/` returns nothing — no hook anywhere refreshes a live session from the database. - `is_authenticated(session)` (`helpers.py:261-266`) is exactly: ``` user = session.get("name") authenticated = session.get("authenticated", False) return authenticated and api.user_exists(user) ``` It re-validates only that the username still exists — never that the session's cached `role`/`perms` still match the current database values. - Inside `apikey_auth` (`helpers.py:393-404`), when no `X-API-Key` header is supplied, `flask.g.user_info` is built
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| PyPI | pyload-ng | — | — |
Remediation: No patched version is listed by GitHub.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.