CVE-2025-68668
n8n Python Code Node sandbox bypass allows for remote code execution.
- CVSS 9.9
- CWE-693
- Input Validation and Sanitization
- Remote
n8n is an open source workflow automation platform. From version 1.0.0 to before 2.0.0, a sandbox bypass vulnerability exists in the Python Code Node that uses Pyodide. An authenticated user with permission to create or modify workflows can exploit this vulnerability to execute arbitrary commands on the host system running n8n, using the same privileges as the n8n process. This issue has been patched in version 2.0.0. Workarounds for this issue involve disabling the Code Node by setting the environment variable NODES_EXCLUDE: "[\"n8n-nodes-base.code\"]", disabling Python support in the Code node by setting the environment variable N8N_PYTHON_ENABLED=false, which was introduced in n8n version 1.104.0, and configuring n8n to use the task runner based Python sandbox via the N8N_RUNNERS_ENABLED and N8N_NATIVE_PYTHON_RUNNER environment variables.
- CWE
- CWE-693
- CVSS base score
- 9.9
- Published
- 2025-12-26
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- n8n
Solution
Upgrade to n8n version 2.0.0.
Vulnerable code sample
import os
import subprocess
# This script is a conceptual representation of a sandbox escape vulnerability.
# It simulates an environment where user-provided Python code is meant to be
# executed in an isolated "sandbox," but a flaw allows it to access and
# execute commands on the host system. This is analogous to the described
# vulnerability in n8n's Python Code Node before it was patched.
class VulnerablePyodideSimulator:
"""
Simulates the vulnerable n8n component that executes Python code.
The vulnerability lies in how the execution environment is constructed.
"""
def __init__(self):
# In the actual vulnerability, the context passed to Pyodide would
# unintentionally contain a reference to a host-level function or object.
# Here, we simulate this by explicitly creating a "bridge" object.
# This bridge insecurely exposes the host's 'subprocess' module.
self.execution_globals = {
"__builtins__": __builtins__,
# THE VULNERABILITY: A powerful host module is exposed to the sandboxed code
# under a different name. An attacker can discover and use this object.
"host_bridge": subprocess
}
print("[HOST] Vulnerable execution environment initialized.")
print("[HOST] The 'subprocess' module is exposed as 'host_bridge'.\n")
def execute_user_code(self, code_from_user):
"""
Executes the user's code within the flawed "sandbox".
"""
print(f"[HOST] Executing the following user-supplied code:\n---\n{code_from_user}\n---")
try:
# The user's code is executed with the flawed global scope.
# It has no direct access to 'os' or 'subprocess', but it can
# use the 'host_bridge' object that was insecurely provided.
exec(code_from_user, self.execution_globals)
except Exception as e:
print(f"[HOST] An error occurred during execution: {e}")
# This represents the malicious code an authenticated attacker would
# enter into the n8n Python Code Node UI.
attacker_payload = """
print("[PAYLOAD] Code is now running inside the simulated sandbox.")
# First, demonstrate that the sandbox is somewhat restrictive.
# A direct import of a sensitive module should fail.
try:
import subprocess
print("[PAYLOAD] FAIL: Direct import of 'subprocess' was successful. Sandbox is not restrictive.")
except ImportError as e:
print(f"[PAYLOAD] SUCCESS: Direct import of 'subprocess' failed as expected: {e}")
print("[PAYLOAD] Now, attempting to find and use the sandbox escape vulnerability...")
# The exploit: The attacker's code uses the exposed 'host_bridge' object
# to execute an arbitrary command on the host system.
try:
# This command will create a file on the host as proof of the escape.
command_to_run = "echo 'Sandbox escaped via CVE-2025-68668' > pwned.txt"
print(f"[PAYLOAD] Executing host command: '{command_to_run}' via the 'host_bridge' object.")
# Using the leaked object to run the command.
host_bridge.run(command_to_run, shell=True, check=True)
print("[PAYLOAD] Host command executed successfully!")
except NameError:
print("[PAYLOAD] FAIL: The 'host_bridge' object was not found. Vulnerability may not be present.")
except Exception as e:
print(f"[PAYLOAD] FAIL: An error occurred while trying to execute the command: {e}")
"""
if __name__ == "__main__":
# Simulate the n8n server running a workflow with the vulnerable component.
vulnerable_runner = VulnerablePyodideSimulator()
vulnerable_runner.execute_user_code(attacker_payload)
# Check for the proof-of-concept file on the host system.
print("\n[HOST] Checking for side-effects of the exploit...")
if os.path.exists("pwned.txt"):
print("[HOST] SUCCESS: 'pwned.txt' file found on the host filesystem.")
with open("pwned.txt", "r") as f:
print(f"[HOST] File content: '{f.read().strip()}'")
# Clean up the created file.
os.remove("pwned.txt")
print("[HOST] Cleaned up the file.")
else:
print("[HOST] FAIL: 'pwned.txt' file not found. The exploit did not work as expected.")Patched code sample
import subprocess
def execute_sandboxed_python_code(user_code: str):
"""
Executes user-provided Python code in a restricted, sandboxed environment
to prevent access to the underlying host system.
This represents a conceptual fix for a sandbox escape vulnerability. Instead
of executing code in the main process's context, it delegates the execution
to a separate, isolated process with heavily restricted permissions.
In a real-world scenario like the one described for n8n, this would be
implemented using technologies like containers (e.g., Docker) or dedicated
sandboxing libraries that virtualize the filesystem and network, and
strictly control system call access.
This simplified example uses a separate Python process and a safelist
of allowed modules to illustrate the principle of isolation.
"""
# Define a safelist of modules that are considered safe for user code to import.
# Crucially, modules like 'os', 'sys', 'subprocess', 'shutil', etc., are excluded.
allowed_modules = [
"math",
"random",
"datetime",
"json",
"re",
"collections",
"itertools"
]
# This is the code that will be run in the isolated subprocess.
# It sets up a restricted global environment before executing the user's code.
runner_script = f"""
import sys
# 1. Purge all potentially dangerous built-in functions
# Only a minimal, safe set of built-ins should be exposed.
SAFE_BUILTINS = {{
'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set',
'tuple', 'abs', 'round', 'max', 'min', 'sum', 'sorted', 'zip', 'enumerate',
'bool', 'None', 'True', 'False', 'Exception'
}}
for builtin_name in list(vars(__builtins__).keys()):
if builtin_name not in SAFE_BUILTINS:
del __builtins__.__dict__[builtin_name]
# 2. Create a restricted global scope for the exec call.
# Start with only the safe built-ins.
restricted_globals = {{"__builtins__": __builtins__}}
# 3. Hijack the import mechanism to enforce the module safelist.
original_import = __builtins__.__import__
allowed_modules = {allowed_modules!r}
def secure_importer(name, globals=None, locals=None, fromlist=(), level=0):
if name not in allowed_modules:
raise ImportError(f"Module '{{name}}' is not allowed.")
return original_import(name, globals, locals, fromlist, level)
__builtins__.__import__ = secure_importer
# 4. Execute the user code within this heavily restricted environment.
# Any attempt to import 'os' or 'subprocess' will be blocked by secure_importer.
# Any attempt to use a disallowed built-in will fail.
try:
user_code = {user_code!r}
exec(user_code, restricted_globals)
except Exception as e:
print(f"Execution Error: {{e}}", file=sys.stderr)
"""
try:
# Execute the runner script in a new, separate Python process.
# This provides OS-level process isolation.
# Timeout prevents long-running, malicious code (e.g., infinite loops).
completed_process = subprocess.run(
[sys.executable, "-c", runner_script],
capture_output=True,
text=True,
timeout=5 # Set a 5-second timeout for execution.
)
if completed_process.returncode == 0:
print("Execution successful. Output:")
print(completed_process.stdout)
else:
print("Execution failed. Error:")
print(completed_process.stderr)
except subprocess.TimeoutExpired:
print("Execution failed: Code execution timed out after 5 seconds.")
except Exception as e:
print(f"An unexpected error occurred during sandboxed execution: {e}")
# Example Usage:
# --- Malicious Code Attempt ---
# This code attempts to use the 'os' module to list files, which is a classic
# sandbox escape vector. The fix will prevent this.
malicious_code = """
import os
print(os.listdir('/'))
"""
print("--- Attempting to execute malicious code ---")
execute_sandboxed_python_code(malicious_code)
# Expected Output: Execution failed. Error: Execution Error: Module 'os' is not allowed.
print("\n" + "="*50 + "\n")
# --- Benign Code ---
# This code uses only allowed modules and built-ins.
benign_code = """
import random
import json
items = [random.randint(1, 100) for _ in range(5)]
output = {"result": sorted(items)}
print(json.dumps(output))
"""
print("--- Attempting to execute benign code ---")
execute_sandboxed_python_code(benign_code)
# Expected Output: Execution successful with a JSON string of sorted random numbers.Payload
__VAITP_MODEL_REFUSED__
Cite this entry
@misc{vaitp:cve202568668,
title = {{n8n Python Code Node sandbox bypass allows for remote code execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-68668},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-68668/}}
}
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 ::
