# Injeção de comando em Rust seguro: o furo real

> Published 2026-10-17T01:31:53.286Z on https://www.crazystack.com.br/pt/p/injecao-de-comando-em-rust-seguro-o-furo-real/
> Source video: https://www.youtube.com/watch?v=T-cPi5qwfk4

Injeção de comando em Rust seguro continua possível: o borrow checker valida ponteiros, lifetimes e aliasing, mas não pergunta se um texto veio de um atacante. Uma string UTF-8 válida entregue a sh -c ou cmd.exe vira instrução executada. Este guia mostra onde a segurança do compilador termina, como a consulta rust/command-line-injection do CodeQL rastreia esse fluxo e quais quatro perguntas de revisão fecham a brecha.

## Injeção de comando em Rust seguro: onde o borrow checker para

Injeção de comando em Rust seguro acontece porque o borrow checker valida memória, não autoridade: ele garante que toda referência aponta para um valor vivo, mas nunca pergunta se aquele texto merecia decidir algo no sistema operacional. O resultado é um binário sem nenhum bloco unsafe que ainda executa o comando escolhido pelo atacante.

Em Rust, ownership dá a cada valor um único dono, e largar esse dono libera os recursos. Referências compartilhadas não mutam dados comuns, e referências mutáveis excluem concorrentes enquanto vivem. É isso que elimina boa parte dos bugs de corrupção de memória — e só isso.

O [Rustonomicon](https://doc.rust-lang.org/nomicon/safe-unsafe-meaning.html) trata exatamente desse escopo: código seguro protege type safety e memory safety, sem prometer que cada escrita em arquivo, requisição de rede ou processo filho seja a que você pretendia. A [Referência do Rust](https://doc.rust-lang.org/reference/behavior-considered-undefined.html) lista data races, ponteiros pendentes ou desalinhados e aliasing quebrado como comportamentos indefinidos — nada ali menciona intenção do usuário.

A confusão é fácil porque os dois fracassos terminam com a máquina comprometida. Um corrompe memória para tomar o controle; o outro pede que o programa use a autoridade que já tem. Rust bloqueia a primeira rota e deixa a segunda inteiramente para o seu design.

## Segurança de memória não é autorização: o exemplo que compila

Segurança de memória e autorização de comando respondem perguntas distintas, e o exemplo vulnerável do próprio GitHub demonstra isso: um valor de requisição entra como texto UTF-8 válido, vira argumento de Command::new e chega a sh -c sem um único unsafe no caminho.

Considere uma rota que aceita a ação rebuild-index. O texto é UTF-8 válido, o empréstimo é válido, o dono é claro. Nada disso prova que quem chamou pode reconstruir qualquer índice. O compilador enxerga uma string correta; a aplicação ainda deve uma decisão de autorização.

O pequeno exemplo mantém invariantes de memória intactas: o input é uma string slice válida, o builder Command tem estado próprio, a saída retorna um Result e até o caminho de erro é tipado. O programa pode ser perfeitamente seguro em memória e catastrófico em efeito.

Um status de sucesso só informa que o sistema operacional fez o que o programa pediu — que é justamente o problema quando um atacante influenciou o pedido. CodeQL e um compilador aprovado não transformam "o processo rodou" em "a ação era permitida".

## Como o CodeQL rastreia injeção de comando em Rust

O [CodeQL](https://codeql.github.com/) é o motor de análise estática do GitHub, e a consulta `rust/command-line-injection` trata injeção de comando em Rust como um problema de caminho: ela declara um source, um sink e um barrier, e pergunta se o rastreamento global de taint acha um fluxo do primeiro ao segundo sem cruzar o terceiro.

Taint é um rótulo preso a dados de origem suspeita. Atribuições, empréstimos, formatações, retornos e chamadas carregam esse rótulo pelo grafo de fluxo de dados. Nenhum exploit precisa rodar: a análise só verifica se a influência sobrevive até a interpretação.

O source pode ser argumento de linha de comando, entrada remota, variável de ambiente, arquivo ou stdin, conforme o modelo de ameaça ativo. O sink é a execução de comando. A pergunta que importa não é o tipo do valor, e sim quem pôde escolhê-lo.

Na release 2.27.0, o CodeQL passou a marcar essa consulta como high precision, com severidade de segurança [9.8 e vínculo a CWE-078 e CWE-088](https://github.com/github/codeql/releases). Essa nota descreve a consequência potencial, não a exploração instantânea de qualquer alerta.

## Source, sink e o que realmente cruza a fronteira do shell

O que transforma dado em controle é a fronteira do processo: Command::arg entrega texto como dado, mas uma opção que pede interpretação transforma esse mesmo argumento em programa. A [documentação de std::process::Command](https://doc.rust-lang.org/std/process/struct.Command.html) confirma o caso seguro.

Segundo a biblioteca padrão, `Command::arg` não passa texto por um shell. Quoting, globbing, expansão de variável e word splitting não têm efeito. Argumentos separados preservam a fronteira de dados — desde que o programa receptor trate o argumento como dado.

Se você escolhe deliberadamente um shell, um interpretador ou outra ferramenta com opção do tipo "avalie isto", o argumento separado volta a cruzar para dentro de uma linguagem. A API cumpriu o contrato; a aplicação mudou o significado.

A mesma documentação carrega um aviso para Windows: a maioria dos programas segue a convenção padrão de argumentos, mas cmd.exe e arquivos em lote decodificam de outra forma, e um argumento malicioso pode executar comandos arbitrários ali. O mesmo cuidado vale para o modo one-shot do `tokei`, que aceita padrões de arquivo a partir da linha de comando.

## PATH, ambiente herdado e o que o processo filho recebe

Caminho de binário e ambiente herdado são vetores independentes da injeção clássica: se o caminho do programa não é absoluto, o sistema operacional busca no PATH, e um atacante que influencie o PATH ou o diretório de trabalho pode redirecionar a execução sem tocar em memória.

A documentação do `Command` recomenda caminho absoluto ou PATH explicitamente controlado quando você precisa de resolução previsível. Vale para binários instalados, wrappers internos e qualquer chamada em que o nome do executável venha de configuração.

Processos filhos herdam o ambiente do pai por padrão. Métodos como `env_remove` e `env_clear` permitem limpar entradas que você não precisa, o que reduz o que um shell comprometido consegue ler — tokens, credenciais de nuvem, variáveis de build.

Diretório de trabalho também é decisão de segurança, não detalhe. Um `cwd` fixo e uma política de privilégio mínimo no sistema operacional limitam o dano quando alguma suposição anterior falha.

## Barriers: por que enum sem campo e allowlist bloqueiam o alerta

Barriers são onde a análise aprende que um caminho perigoso foi restringido. A extensão Rust do CodeQL trata valores numéricos, booleanos e enums sem campo como barriers, porque esses tipos não conseguem carregar uma string de comando arbitrária.

A consulta também reconhece verificações de pertencimento e de igualdade bem-sucedidas como guardas de sanitização. Nos testes do GitHub, uma allowlist com `cat`, `git` e `ls` deixa o ramo dentro da verificação limpo, enquanto o ramo fora dela continua gerando alerta.

O ramo importa muito. Uma função de validação que calcula true ou false mas não controla o caminho até o sink é decoração: o CodeQL modela o lado bem-sucedido da guarda porque o comando precisa estar restringido em toda rota capaz de alcançar a execução.

Essa é a correção mais forte que os próprios testes sugerem. Em vez de limpar um comando de formato livre torcendo para cobrir todo metacaractere, converta texto não confiável em uma escolha pequena e confiável e mapeie essa escolha para programas e argumentos fixos. A autoridade passa a ser explícita.

## Checklist de defesa para Command::new em produção

A defesa eficaz vive em camadas, e nenhuma delas substitui as outras: o borrow checker protege a memória, o formato da API mantém argumentos separados, a validação limita escolhas, a configuração do processo limita influência ambiente e as permissões do sistema operacional limitam o dano residual.

Para cada chamada de `Command::new` no seu código, responda quatro perguntas antes de aprovar o pull request.

1. **Quem escolhe o executável?** Se a resposta inclui qualquer dado externo, fixe o binário e resolva o caminho de forma absoluta.
2. **Quem escolhe cada argumento?** Prefira argumentos literais ou valores vindos de um enum fechado.
3. **Algum valor alcança um parser de shell?** Se sim, existe um `-c`, um `eval` ou um arquivo em lote no meio.
4. **O que o processo filho acessa?** Ambiente herdado, diretório de trabalho e privilégio precisam ser decididos, não aceitos por padrão.

Use uma tabela mental simples para escolher o nível de risco antes de escrever a chamada:

| Forma da chamada | Interpretação | Risco |
| --- | --- | --- |
| `Command::new("cat").arg(path)` | nenhuma, argumento como dado | baixo |
| `sh -c "cat $path"` | shell interpreta o argumento | alto |
| `cmd.exe /c "..."` | decodificação própria de argumentos | alto |
| enum → binário fixo + literais | escolha enumerada, sem shell | baixo |

Em muitos casos, a melhor saída é não criar processo nenhum. Uma biblioteca Rust que faz a operação substitui o interpretador inteiro e elimina uma classe de confusão de parser. Se precisar lançar processo, mantenha o executável fixo, passe cada argumento separado e rode com o menor privilégio disponível.

Um exemplo concreto: um endpoint de download de relatório aceita identificador e formato, não comando. O enum de formato mapeia para uma operação fixa de biblioteca, o valor desconhecido é rejeitado e o caminho do relatório vira argumento literal depois de canonicalizado dentro de um diretório permitido.

## O que o CodeQL não vê e como usar o alerta na revisão

Um scan limpo não prova ausência de injeção de comando em Rust: significa que o analisador não encontrou caminho com a extração e os modelos atuais. Wrappers internos, frameworks próprios, sanitizadores caseiros e lançadores de comando podem precisar de modelos declarados como dados.

Um modelo ausente esconde um caminho; um modelo frouxo adiciona ruído. Vale manter code review e testes automatizados justamente em volta da fronteira de confiança, sobretudo para wrappers que a consulta ainda não entende.

Na prática, trate cada alerta como um caminho a inspecionar, não como veredito. Confirme a origem do input, a API de processo, a existência de parsing por shell, a validação que domina aquele ramo e a identidade do processo filho.

O [CWE-078 do MITRE](https://cwe.mitre.org/data/definitions/78.html) descreve a classe de falha sem depender de linguagem. Vale lembrar que o CodeQL não é a única camada estática disponível — o Clippy cobre outra faixa de problemas e não substitui taint tracking.

## FAQ sobre injeção de comando em Rust

- **Rust seguro impede injeção de comando?** Não. O borrow checker garante validade de referências, lifetimes e aliasing, mas não verifica se um texto tinha autorização para virar ação no sistema operacional. Um caminho sem unsafe pode entregar entrada não confiável a um shell e executar o comando do atacante.

- **O que faz o CodeQL detectar injeção de comando em Rust?** A consulta `rust/command-line-injection` faz rastreamento de taint de um source, como linha de comando ou entrada remota, até um sink de execução, e só deixa de alertar quando um barrier interrompe esse fluxo. O resultado é high precision, com severidade 9.8 e vínculo a CWE-078 e CWE-088 na release 2.27.0.

- **`Command::arg` é seguro então?** Argumentos separados não passam por shell, então quoting, globbing e expansão de variável não têm efeito. A proteção depende do receptor tratar o argumento como dado: se você escolhe `sh -c` ou `cmd.exe /c`, a interpretação volta e o risco retorna.

- **Qual a correção mais robusta?** Trocar comando de formato livre por uma escolha enumerada que mapeia para binários e argumentos fixos, com caminho absoluto, ambiente limpo e privilégio mínimo. Quando existir biblioteca Rust para a operação, dispensar o processo filho elimina a classe inteira de confusão de parser.

- **Posso confiar que um scan limpo significa que não há vulnerabilidade?** Não. O resultado depende dos modelos e da extração vigentes: wrappers e sanitizadores próprios podem não estar modelados. Mantenha revisão de código, testes na fronteira de confiança e restrições do sistema operacional como camadas adicionais.

[Source video](https://www.youtube.com/watch?v=T-cPi5qwfk4)
