13 de ago. de 2025·7 min de leitura

Varredura de risco de RCE para apps Node: identifique código perigoso rapidamente

Varredura de risco RCE para apps Node para identificar eval, chamadas inseguras a child_process, injeção de template e imports dinâmicos arriscados frequentemente encontrados em código gerado por IA.

Varredura de risco de RCE para apps Node: identifique código perigoso rapidamente

Como RCE aparece em um app Node real

Execução remota de código (RCE) significa que um atacante pode fazer seu servidor executar código que você não pretendia. Não é só ler um arquivo ou roubar um token — é executar comandos ou carregar código de forma que lhe dê controle. Se seu app é acessível pela internet, RCE é um dos caminhos mais rápidos de um pequeno bug para um takeover completo.

Em apps Node, RCE costuma aparecer quando entrada não confiável é tratada como instrução. Um exemplo clássico é um recurso “roda uma ferramenta pra mim”: o usuário envia texto, o servidor monta um comando de shell, e o child_process o executa. Se o código não controla estritamente o que é permitido, uma requisição maliciosa pode transformar esse comando em outra coisa completamente diferente.

Código gerado por IA tende a incluir atalhos arriscados porque tenta ser útil. Pode adicionar eval para “parsear” dados, construir comandos de shell com templates de string, ou carregar módulos dinamicamente com base em um parâmetro da requisição. Esses padrões funcionam em demo, mas são perigosos em produção.

Uma varredura de risco é uma forma rápida de localizar trechos que parecem poder executar entrada. Ela aponta onde olhar primeiro, não se você está 100% seguro. Cada achado ainda precisa de verificação humana para confirmar se a entrada do usuário realmente pode alcançar a linha perigosa.

Atacantes normalmente alcançam RCE por pontos de entrada cotidianos:

  • Requisições HTTP (query, body, headers)
  • Uploads de arquivos (nomes, caminhos, conteúdos)
  • Webhooks ("eventos" de terceiros em quem você confia demais)
  • Painéis administrativos (menos escrutínio, ações poderosas)
  • Jobs em background (mensagens de filas ou entradas de cron)

Onde a entrada não confiável entra no seu código

A maioria dos bugs de RCE começa do mesmo jeito: seu app trata dados de outra pessoa como se fossem seguros. Ao escanear riscos de RCE em um app Node, comece mapeando todos os lugares onde dados podem entrar, mesmo que normalmente venham da sua própria equipe.

Os pontos óbvios são requisições HTTP. Tudo que o usuário pode alterar é não confiável: query strings, params de rota, corpos JSON, formulários, headers (especialmente os usados para feature flags ou modos de debug), cookies, dados de sessão e arquivos enviados (incluindo nomes de arquivo).

Nem toda entrada vem pela web. Protótipos rápidos frequentemente incluem scripts auxiliares e recursos administrativos que contornam checagens normais, e depois acabam em produção. Preste atenção a jobs em background e tarefas de cron que leem do banco, filas de mensagens, payloads de webhooks de terceiros, painéis administrativos e endpoints “apenas internos”, e scripts CLI que aceitam argumentos ou leem variáveis de ambiente.

"Ferramentas internas" ainda importam. Tokens são roubados, VPNs são usadas indevidamente, e um cookie administrativo vazado pode transformar um endpoint interno em um acessível pela internet.

Um modelo mental útil é:

input -> parsing -> execução

Parsing é onde tipos mudam (string para JSON, JSON para objeto, objeto para template). Execução é onde o perigo aumenta (construir comandos, avaliar código, carregar módulos). Um endpoint só de suporte que aceita JSON como { "report": "weekly" } pode virar RCE mais tarde se alguém adicionar child_process ou uma etapa de render de template que use o mesmo campo.

Um plano simples para escanear riscos de RCE

Você não precisa de ferramentas sofisticadas para que uma varredura de risco seja útil. Comece encontrando os formatos de código que mais levam a “executar o que o usuário enviou”, especialmente em código gerado por IA onde atalhos inseguros aparecem.

Comece com uma busca por palavras-chave para gerar uma lista curta de arquivos a revisar:

rg -n "\\beval\\b|new Function|child_process|exec\\(|execSync\\(|spawn\\(|spawnSync\\(|fork\\(|vm\\.run|ejs|pug|handlebars|nunjucks|import\\(|require\\(\" .

Então transforme os resultados em um mapa de fluxo de entrada. Para cada ocorrência, responda duas perguntas:

  • Que dados podem alcançar essa linha?
  • Quem controla esses dados?

Acompanhe fontes comuns como params de requisição, headers, cookies, payloads de webhook e qualquer coisa lida do banco que tenha vindo originalmente de usuários.

Para triagem, foque na combinação de alcançabilidade e poder: rotas acessíveis pela internet (incluindo webhooks), qualquer uso de execução de comandos do SO ou carregamento de código (child_process, vm, import/require dinâmicos), e lugares onde strings são construídas a partir de entrada sem allowlist. Confirme o que está realmente ativo em produção, porque código morto e scripts só de dev são menor prioridade que código atingido por requisições reais.

Encontre eval e Function que podem executar entrada

Comece caçando lugares onde código é construído a partir de strings. Os suspeitos usuais são eval() e new Function(). Eles transformam um texto em algo que o servidor executa. Se um atacante puder influenciar esse texto, pode potencialmente rodar seu próprio código.

Marque padrões como:

  • eval(userInput) ou eval(someVar)
  • new Function("return " + expr)()
  • setTimeout("doThing(" + x + ")", 0) e setInterval("...", 1000)
  • “avaliadores de expressão” que concatenam strings antes de executá-las

"Só avalia expressões pequenas" ainda é perigoso. Uma calculadora aparentemente inofensiva pode virar acesso a process.env, leitura de arquivos, chamadas de rede ou pior se a string puder ser moldada por um atacante.

Além disso, a entrada não precisa vir direto do corpo da requisição. Muitas vezes ela entra por config files, campos de banco, conteúdo de CMS, feature flags ou templates que armazenam regras editáveis pelo usuário. Em protótipos rápidos você às vezes encontra um motor de regras rápido como eval(dbRow.rule) que funcionou em demo e foi enviado.

Substituições mais seguras dependem do que você tenta fazer. Em geral, prefira um conjunto fixo de operações mapeadas para funções reais, ou armazene regras como dados (por exemplo JSON) e interprete com validação estrita. Se realmente precisar de uma linguagem de expressões, use um parser real e só avalie nós suportados.

Verifique child_process para injeção de comandos

Se seu app usa child_process do Node para rodar comandos do sistema, trate isso como um risco alto de RCE. O perigo não é apenas “usar um shell”. O perigo é deixar qualquer texto controlado pelo usuário virar parte do comando.

As chamadas de maior risco são exec e execSync, e spawn quando shell: true está definido. São fáceis de usar de forma incorreta porque aceitam uma única string de comando. Uma vez que você constrói essa string com + ou template strings, um usuário pode inserir operadores extras (como ;, &&, |) e rodar algo que você não pretendia.

Um deslize comum no mundo real é um endpoint “converter arquivo” que faz exec(convert ${req.body.path} -resize 200x200 out.png). Se path contém image.png; cat /etc/passwd, o shell interpreta como dois comandos.

Um padrão mais seguro é evitar o shell e passar um array de argumentos. Por exemplo: spawn('convert', [inputPath, '-resize', '200x200', outPath], { shell: false }). Ainda é preciso validar inputPath, mas você elimina a maioria dos truques de parsing do shell.

Ao escanear, procure sinais como strings de comando construídas a partir de campos de requisição, shell: true, junção manual de argumentos (args.join(' ')) e helpers de “escape” que só substituem alguns caracteres.

Se adicionar logs para ajudar a triagem, registre o que importa (com cuidado): o nome do comando, o array de args (ou a string completa se for necessário) e exatamente de onde veio a entrada (rota e nome do campo). Não despeje segredos em logs.

Detecte caminhos de injeção de template no servidor

Herdou um protótipo quebrado?
Se Lovable, Bolt, v0, Cursor ou Replit bagunçaram seu protótipo, tornamos ele pronto para produção.

Injeção de template server-side (SSTI) acontece quando seu app monta um template a partir de entrada do usuário e o renderiza no servidor. Se o engine de templates consegue rodar expressões, um atacante pode transformar um recurso de “mensagem personalizada” em execução de código.

Um exemplo simples: você deixa usuários salvar um template de e-mail, e então compila ou renderiza o que eles digitarem. Se o engine suporta {{ someExpression }} ou chamadas de helper, o usuário pode ler segredos, chamar funções ou encadear para outras APIs perigosas.

Em uma varredura rápida, procure lugares onde strings controladas pelo usuário viram templates, partials, layouts ou nomes de helpers. Padrões comuns incluem compilar ou renderizar input direto do usuário, passar dados da requisição como locals sem allowlist (por exemplo res.render("view", req.body)), deixar usuários escolherem um partial/layout por nome concatenando caminhos, registrar helpers dinamicamente a partir de dados de requisição, ou renderizar markdown/handlebars/ejs armazenado a partir de um campo de formulário sem camada de segurança.

Mitigações que costumam funcionar:

  • Não compile templates a partir de texto cru do usuário. Armazene conteúdo, mas renderize como texto simples ou um subconjunto de marcação seguro.
  • Mantenha um mapa de variáveis estrito para templates. Não passe objetos inteiros como req, res ou process.
  • Trate nomes de templates como caminhos de arquivo: allowlist templates conhecidos e rejeite todo o resto.

Revise imports dinâmicos e carregamento de módulos arriscados

Carregamento dinâmico de módulos é conveniente, mas também é uma forma comum de RCE entrar em apps Node, especialmente quando o código foi gerado rapidamente. Se qualquer parte do nome do módulo ou caminho de arquivo vem do usuário, trate como entrada não confiável.

Os padrões mais perigosos parecem inocentes à primeira vista: import(userInput), require(userInput) ou construir um caminho como require('./plugins/' + name). As pessoas assumem “só carrega arquivos locais”, mas atacantes podem abusar de truques de path, locais de arquivo inesperados ou alcançar código que você não queria expor.

Escaneie por:

  • import(something) onde something não é literal de string
  • require(something) onde something é construído a partir de variáveis
  • require(path.join(base, userValue)) e qualquer risco de traversal como ../
  • features de “plugin” que carregam módulos por nome
  • leitura de nome de arquivo a partir de requisição, banco ou config e então carregar

Se realmente precisar de carregamento dinâmico, mantenha explícito. Use uma allowlist hardcoded que mapeia nomes para caminhos exatos, rejeite qualquer coisa fora do mapa e nunca passe input cru para require() ou import().

Não ignore problemas de suporte que viabilizam RCE

Resolver vazamentos de autenticação e segredos
Corrigimos autenticação quebrada e removemos chaves expostas que pioram RCE.

Mesmo se encontrar um bug óbvio de execução, atacantes normalmente precisam de alguns fatores auxiliares para transformar isso em incidente real. Uma boa varredura inclui problemas de suporte que tornam a exploração fácil e a limpeza difícil.

Comece pelos segredos. Protótipos rápidos costumam deixar chaves API, segredos JWT, URLs de banco e tokens cloud em texto claro, arquivos env de exemplo ou logs. Se um atacante obtiver qualquer execução de código, segredos expostos permitem saltos rápidos para seus dados e outros sistemas.

Permissões importam tanto quanto. Se a aplicação roda como um usuário que pode ler/escrever demais, um RCE vira “modificar o servidor”. Fique atento a permissões amplas de arquivos, diretórios de app graváveis e roles cloud com privilégios excessivos.

Também verifique configurações de produção que abrem portas silenciosamente: modo de desenvolvimento em produção, flags de debug ativas (erros verbosos, stack traces, hot reload), rotas administrativas escondidas ainda no lugar, CORS inseguro e diretórios de upload/temp que são graváveis e expostos.

Finalmente, dependências podem ser o elo mais fraco. Pacotes desatualizados com CVEs de RCE conhecidos podem transformar uma rota inofensiva em comprometida. Uma checagem rápida do lockfile e das advisories faz parte do trabalho.

Erros comuns ao tentar corrigir riscos de RCE

A maneira mais rápida de perder tempo é consertar o que vê sem provar como o código é alcançado. Um helper com aparência limpa ainda pode ser perigoso se estiver atrás de uma rota, middleware, job ou webhook que aceita entrada externa.

Uma armadilha comum é assumir “não é alcançável” porque você não vê um botão na UI. Trace o caminho mesmo assim: request -> router -> middleware -> controller -> helper. Faça o mesmo para workers em background e handlers de webhook. Muitos incidentes reais começam em caminhos raramente testados, como um webhook de pagamento, callback OAuth ou fila interna de jobs.

Outro erro é tratar o sintoma em vez da causa. Escapar saída ou adicionar encoding não torna eval, Function, exec ou spawn seguros se entrada não confiável pode alcançá-los. Se o scan encontrar execução dinâmica, o objetivo geralmente é removê-la, não “sanitizar”.

"Sanitizadores" baseados em regex são outro beco sem saída. Se a correção depende de “bloquear esses caracteres”, assuma que alguém vai contornar com espaços em branco, aspas, metacaracteres do shell ou truques de codificação.

Hábitos que evitam reincidência:

  • Prove atingibilidade traçando rotas, middleware, workers e webhooks
  • Substitua execução dinâmica por comandos fixos, templates fixos ou bibliotecas avaliadas
  • Valide entrada por tipo e intenção (IDs, enums, esquemas estritos), não por regex
  • Adicione testes para payloads maliciosos para que o problema não volte

Checklist rápido antes de enviar

Use isto após a varredura e novamente antes do release. O objetivo é simples: nada no servidor deve ser capaz de transformar entrada do usuário em código, comando de shell ou caminho de módulo.

  • Sem eval, new Function ou setTimeout/setInterval baseados em string no código do servidor.
  • Sem exec ou execSync. Para spawn/execFile, passe args como array e não habilite shell: true.
  • Não compile templates do lado servidor a partir de texto fornecido pelo usuário.
  • Qualquer import/carregamento dinâmico está em allowlist e não aceita caminhos construídos pelo usuário.
  • Validação de entrada acontece nas fronteiras (handlers HTTP, consumidores de fila, webhooks), com regras claras por campo.

Um teste simples de sanidade: finja que um atacante controla um query param, um header e um campo JSON. Algum desses valores pode alcançar um executor de código (eval), um executor de comando (child_process), um compilador de template ou um caminho de import sem ser rejeitado?

Exemplo: uma funcionalidade pequena que acidentalmente vira RCE

Fechar caminhos de injeção de comando
Substitua strings de comando em child_process por argumentos seguros e allowlists estritas.

Um problema comum do mundo real é uma página administrativa interna construída por IA com um campo “Run maintenance”. A ideia é inofensiva: digite algo como reindex-users ou clear-cache, clique em Run e o servidor executa.

O primeiro problema é como o recurso é conectado. O endpoint frequentemente pega req.body.command e passa para child_process.exec(), ou constrói um caminho de script a partir dele e faz import() do arquivo. Se um atacante pode atingir esse endpoint (autenticação fraca, cookie admin vazado, falta de checagem de papel), ele pode tentar inputs como reindex-users && cat .env ou ../../../../tmp/payload e você tem RCE.

Um scan tipicamente vai flagar algumas coisas rapidamente: child_process.exec() alimentado por dados de requisição (mesmo depois de “sanitizar”), imports dinâmicos ou require() construídos a partir de strings, renderização de template que compila input do usuário, e helpers como eval() ou new Function() deixados por geradores.

Depois do scan, confirme os detalhes manualmente: a rota é alcançável sem checagem administrativa rígida, os inputs são realmente controláveis e o que roda no servidor (shell, node ou um executor de script)? O código perigoso frequentemente está por trás de feature flags “temporárias”.

O que consertar primeiro é o que remove as primitivas de execução:

  1. Delete a caixa de comando em texto livre e substitua por uma allowlist de ações nomeadas.
  2. Troque exec por APIs mais seguras (ou faça o trabalho em processo com funções normais).
  3. Trave a rota a um papel admin real e registre cada ação.
  4. Rode o serviço com privilégio mínimo e remova segredos do ambiente de runtime.

O resultado final é simples: menos caminhos para alcançar o código, e nenhuma forma direta de executar entrada.

Próximos passos: transformar achados em código seguro e pronto para produção

Trate cada achado como uma decisão de produto, não só um bug. Alguns padrões arriscados valem a pena serem corrigidos; outros é melhor remover porque voltarão a aparecer conforme o código evolui.

Uma forma prática de decidir é rotular cada problema:

  • Patch: você pode tornar a abordagem atual segura com validação estrita, allowlists e APIs mais seguras.
  • Refactor: a funcionalidade é necessária, mas o design convida a erros.
  • Remover/substituir: a funcionalidade existe principalmente porque um gerador adicionou.

Após os consertos, escreva uma nota de segurança curta no repositório. Seja direto e específico: nada de eval/Function com dados de requisição, nada de strings controladas por usuário em child_process, templates não devem compilar input do usuário, imports dinâmicos só de allowlist. Isso ajuda o futuro você e agiliza revisão de código.

Adicione guardrails leves para evitar que os mesmos problemas voltem. Uma busca salva ou checklist de revisão por eval, new Function, child_process.exec, compilação de templates e import() dinâmico a partir de variáveis pega muita coisa.

Se você herdou um protótipo gerado por ferramentas como Lovable, Bolt, v0, Cursor ou Replit e quer uma segunda opinião rápida, FixMyMess (fixmymess.ai) foca em diagnosticar e reparar padrões arriscados como estes, e depois verifica os consertos com revisão humana especializada para que o app resista em produção.

Perguntas Frequentes

What exactly counts as RCE in a Node app?

RCE (remote code execution) é quando alguém consegue fazer seu servidor executar código ou comandos que você não pretendia. Geralmente é pior que vazamento de dados porque pode levar ao controle total do app, da máquina onde roda e de quaisquer segredos que o app acesse.

What’s the fastest way to do an RCE risk scan without fancy tools?

Comece identificando onde dados não confiáveis entram: requisições HTTP, webhooks, uploads de arquivos, ferramentas administrativas e jobs em background. Em seguida, procure por “primitivas de execução” como eval, new Function, child_process, compilação de templates e import()/require() dinâmicos e trace se dados controlados por usuário podem alcançar esses pontos.

Will a keyword scan tell me if I’m definitely vulnerable?

Não. Um scan por palavras-chave encontra padrões suspeitos, não exploits confirmados. Você ainda precisa traçar o fluxo dos dados e confirmar a atingibilidade em produção, porque alguns resultados podem ser código morto ou só executados com inputs confiáveis.

Why are internal admin endpoints such a common RCE problem?

Mesmo “internos” podem ficar acessíveis quando tokens são roubados, cookies vazam ou uma má configuração expõe a rota. Trate endpoints administrativos como superfícies de ataque reais: autenticação estrita, validação rígida de entrada e nada de execução livre a partir de texto.

Is `eval()` always a security bug, or can it be safe?

É perigoso sempre que a string avaliada puder ser influenciada por usuários, mesmo indiretamente via campos de banco, config, CMS ou feature flags. A maioria das correções seguras remove avaliação de strings e substitui por uma allowlist pequena de operações suportadas implementadas como funções reais.

What’s the real risk with `child_process.exec()` and command strings?

exec e execSync têm alto risco porque executam uma string de comando via shell, tornando fácil injetar operadores extras. Prefira execução sem shell com arrays de argumentos (como spawn ou execFile) e, ainda assim, valide inputs como nomes de arquivo e IDs para que atacantes não apontem a ferramenta para arquivos inesperados.

How do I know if my template engine usage can become RCE?

SSTI ocorre quando você compila ou renderiza um template construído a partir de entrada do usuário e o motor de templates suporta expressões ou helpers. Um padrão seguro é armazenar o texto do usuário como conteúdo e renderizá-lo como texto simples (ou um subconjunto de marcação restrito), em vez de tratá-lo como template do servidor.

Why are dynamic `import()` or `require()` patterns considered RCE risks?

É arriscado quando qualquer parte do nome do módulo ou caminho vem de dados controlados pelo usuário, mesmo que “devesse” ser local. A correção prática é mapear nomes permitidos para módulos exatos em código e rejeitar tudo que não esteja na allowlist, em vez de passar input cru para require() ou import().

Can I just “sanitize” input to make RCE issues go away?

Não de forma confiável. Bloquear caracteres como ; ou && é fácil de contornar com codificação, espaços em branco ou regras de parsing diferentes do shell, e não resolve injeção de template ou execução dinâmica. A abordagem mais segura é remover a primitiva de execução ou limitá-la a ações fixas e allowlistadas.

What should I fix first if a scan finds multiple risky spots?

Priorize qualquer coisa alcançável pela internet (incluindo webhooks) que possa executar código, rodar comandos do SO, compilar templates ou carregar módulos dinamicamente. Se você herdou código gerado por IA e precisa de uma revisão rápida, equipes como FixMyMess se concentram em diagnosticar esses padrões, consertá-los e verificar os fixes com checagens humanas para tornar o app seguro para produção.