TL;DR
We discovered a critical vulnerability (GHSA-6cqr-8cfr-67f8 / CVE-2026-25049) in n8n that allows authenticated users to achieve remote code execution (RCE) via sandbox escape. With full control over the n8n instance, attackers can read files, execute system commands, access databases, and exfiltrate credentials stored in n8n's credential vault.
The vulnerability stems from a flaw in how n8n sanitizes workflow expressions. In all versions prior to 2.5.2 and 1.123.17, the sanitization function assumes keys in property accesses are strings in attacker-controlled code. This assumption is correctly encoded in a TypeScript annotation, but it is not enforced through runtime type checking. Attackers can exploit this type confusion to bypass sanitization controls entirely, enabling arbitrary code execution attacks.
The vulnerability has been patched in n8n version 2.5.2 and we strongly recommend immediate upgrades for all n8n instances. However, there were ten other vulnerabilities disclosed on the same day for n8n, so strongly advise taking prompt action!
Affected versions
How most n8n versions are vulnerable to type confusion attacks
n8n is a popular workflow automation platform that allows users to create complex automation workflows by connecting various services and APIs. A core feature of n8n is its expression system, which enables users to write JavaScript expressions that are evaluated within workflows. These expressions can access workflow data, perform calculations, and manipulate data structures.
Given that these expressions are user-provided and evaluated on the server, n8n implements sanitization mechanisms to prevent malicious code execution. However, as we'll demonstrate below, relying solely on TypeScript's type system for security guarantees is insufficient when the runtime behavior doesn't match the compile-time assumptions.
This vulnerability is particularly noteworthy because it highlights a critical gap between static type checking and runtime security: Even when developers correctly annotate the sanitizer's input parameter as a string, attackers can still pass non-string values at runtime, thereby bypassing the security controls.
n8n sanitizes workflow expressions to prevent access to dangerous object properties. The sanitization logic is implemented in the isSafeObjectProperty function located in packages/workflow/src/utils.ts:
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf',
'mainModule',
'binding',
'_load',
]);
export function isSafeObjectProperty(property: string) {
return !unsafeObjectProperties.has(property);
}This function checks whether a property name is in a denylist of unsafe properties (such as __proto__, constructor, toString, etc.). The function signature explicitly declares that `property` must be a string type.
The vulnerability arises from a mismatch between TypeScript's compile-time type system and JavaScript's runtime behavior. While TypeScript enforces that property should be a string at compile time, this enforcement is limited to values that are present in the code during compilation. TypeScript cannot enforce these type checks on runtime attacker-produced values. When attackers craft malicious expressions at runtime, they can pass non-string values (such as objects, arrays, or symbols) that bypass the sanitization check entirely.
The critical flaw lies in how the sanitization check interacts with JavaScript's type coercion. Under the hood, the has() method performs a strict equality check (===), which means it will only match exact string values stored in the unsafeObjectProperties Set. However, when a non-string value (such as an array like ["__proto__"]) is passed to isSafeObjectProperty, the strict equality check fails to match it against the blacklist, allowing it to pass through the sanitization.
The vulnerability becomes exploitable because downstream code that uses this property value may coerce it back to a string or access it in ways that convert the non-string value into the exact blacklisted property names. For example, when the property is used in bracket notation like obj[property], JavaScript's type coercion can convert ["__proto__"] back to the string "__proto__", effectively bypassing the sanitization check while still accessing the dangerous property. Thus, when a non-string value is passed to isSafeObjectProperty, the `unsafeObjectProperties.has() check may fail in unexpected ways.
The Proof of Concept for this vulnerability
An attacker can exploit this vulnerability by crafting malicious workflow expressions. Here are two example payloads:
Prototype Pollution:
{{ {}[["__proto__"]].polluted = 23 }}Arbitrary Code Execution:
{{ {}[["toString"]][["constructor"]]("p","return p.env")(process) }}These expressions use bracket notation with arrays to bypass the sanitizer's string-based checks, as described above. The sanitizer expects string property names, but by using array access patterns, attackers can pass non-string values that circumvent the input validation.
The arbitrary code execution payload is particularly dangerous because it invokes the Function constructor with a reference to the process object. The expression {}[["toString"]][["constructor"]] accesses the toString method of an empty object, then retrieves its constructor property (which is the Function() constructor), bypassing the sanitizer's check for the string "constructor". The Function constructor is then called with two arguments: "p" (a parameter name) and "return p.env" (the function body). When this function is immediately invoked with process as the argument, it returns process.env, giving the attacker access to environment variables.
More critically, attackers can modify the function body to execute arbitrary OS commands. However, executing commands is more complex than simply calling require('child_process').execSync(), as n8n may restrict access to certain Node.js modules. Instead, attackers can leverage process.binding('spawn_sync') to access low-level spawning capabilities and reconstruct command execution functionality, similar to techniques we used in prior sandbox escape research [pdf]. By accessing process.binding(), attackers can bypass module restrictions and directly invoke system-level spawning functions, enabling arbitrary command execution with the privileges of the n8n process.
This means that an attacker who can add such an expression to a workflow gains RCE capabilities, essentially taking over the server when the workflow executes and the expression is evaluated.
Why TypeScript types don't prevent this type confusion attack
This vulnerability demonstrates a critical limitation of relying on TypeScript's type system for security. TypeScript types are erased at runtime - they exist only during compilation and provide no runtime enforcement. Even when a developer correctly annotates the function parameter as string, JavaScript's dynamic nature allows any value type to be passed at runtime.
This is a well-documented issue in type system research. As noted in prior research [pdf] on the dangers of type systems, static type checking cannot prevent runtime type confusion attacks when the runtime environment doesn't enforce the type constraints.
The fix: n8n 2.5.2 has proper runtime checking in the sanitization function
n8n has addressed this vulnerability in version 2.5.2 by implementing proper runtime type checking in the sanitization function. The fix ensures that the property parameter is actually a string at runtime before performing the safety check, preventing type confusion attacks.
The fix likely involves adding runtime type validation, such as:
export function isSafeObjectProperty(property: string) {
if (typeof property !== 'string') {
return false; // Reject non-string values
}
return !unsafeObjectProperties.has(property);
}This demonstrates an important security principle: Never trust type annotations alone for security-critical code paths. Manual runtime validation is essential when processing untrusted input in TypeScript.
Mitigation the RCE
First, patch immediately by upgrading to n8n version 2.5.2 or later. This vulnerability, combined with CVE-2026-21858 and ongoing supply chain attacks targeting the n8n ecosystem, makes prompt patching critical. Review and audit existing workflows for suspicious expressions, particularly those using bracket notation like [["__proto__"]] or accessing dangerous properties like constructor or process.
We also recommend considering the following actions, in addition to migrating to the fixed version:
Access Control and Isolation: Implement strong authentication (MFA, SSO) and enforce strict access controls with least privilege principles - restrict who can create, modify, or execute workflows. Isolate n8n instances from critical systems by running them in containers with minimal privileges, read-only filesystems where possible, and non-root users. Additionally, use network segmentation and firewall rules to limit outbound connections and prevent n8n from accessing sensitive internal resources.
Monitoring and Detection: Given the high exploitation feasibility of this vulnerability - attackers can easily craft malicious expressions using bracket notation to bypass sanitization - implement monitoring to detect exploitation attempts. Monitor for workflow expressions using non-string property access patterns (like [["constructor"]] or [["__proto__"]]), Function constructor invocations with process object references, process.binding() calls, and prototype pollution attempts. Alert on unexpected outbound network connections, credential access patterns, and workflow modifications that introduce suspicious expressions. The vulnerability's low attack complexity means exploitation is likely, making detection critical for incident response.
Migration Considerations: For organizations evaluating workflow automation platforms, consider the security implications of executing untrusted code and centralizing credentials. This vulnerability is part of a broader pattern of sandbox escape vulnerabilities in workflow automation platforms. Research has shown that sandbox escapes are particularly dangerous in workflow automation contexts.
Takeaways for n8n shops
Type Safety ≠ Runtime Safety: TypeScript and other static type systems provide no runtime security guarantees. Type annotations are erased at runtime and cannot prevent attackers from passing non-string values when functions expect strings. Security-critical code must always include runtime type validation, regardless of static type annotations.
Include type confusion attacks in security testing—test with non-string values even when functions expect strings.
Defense in Depth: This vulnerability demonstrates why multiple layers of validation are crucial. Even if one layer (TypeScript types) appears strong, additional runtime checks are necessary when processing untrusted input. Pay special attention to sanitization functions during code review, looking for assumptions about input types that aren't enforced at runtime.



What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:








