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 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 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 é 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. 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 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.
- Quem escolhe o executável? Se a resposta inclui qualquer dado externo, fixe o binário e resolva o caminho de forma absoluta.
- Quem escolhe cada argumento? Prefira argumentos literais ou valores vindos de um enum fechado.
- Algum valor alcança um parser de shell? Se sim, existe um
-c, umevalou um arquivo em lote no meio. - 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 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-injectionfaz 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ê escolhesh -coucmd.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.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits