Interpolação de shell: evite que entradas do agente virem comandos
A interpolação de shell pode transformar caminhos e nomes de recursos controlados por agentes em sintaxe executável. Crie ações locais e SSH mais seguras com arrays e validação.

Um agente não precisa de uma ferramenta de shell para provocar a execução de comandos. Dê a ele um caminho, uma branch, uma tag de imagem, um hostname ou um nome de recurso. Depois, encaixe esse valor em uma string de comando em algum ponto posterior, e você terá dado a um texto não confiável a chance de alterar o programa que será executado.
A solução não é criar uma função de aspas mais inteligente. É parar de tratar a construção de comandos como formatação de strings. Uma ação deve nomear uma operação fixa, aceitar campos tipados com regras restritas, chamar um programa com um vetor real de argumentos e manter a autorização separada da análise. Já revisei relatos de incidentes suficientes para dizer isso sem rodeios: uma ferramenta que aceita um comando livre porque é «somente para o nosso agente» acabará aceitando também as instruções de outra pessoa.
Os agentes tornam esse problema mais fácil de alcançar. Eles consomem texto de issues, conteúdo de repositórios, saídas de ferramentas, páginas da web e mensagens de chat. Qualquer uma dessas entradas pode influenciar o próximo argumento. A intenção do agente não muda o problema. O limite que chega a um sistema operacional ou host remoto precisa presumir que um atacante pode controlar sua entrada.
A interpolação de shell transforma dados em sintaxe
A interpolação de shell acontece quando o código constrói texto para um interpretador de comandos em vez de fornecer argumentos separados a uma API de processos. Quando um shell recebe esse texto, aplica suas próprias regras de linguagem. Separadores podem iniciar outro comando, substituições de comando podem executar um comando aninhado, redirecionamentos podem alterar arquivos e expansões podem transformar um valor aparente em várias palavras.
Considere um agente encarregado de inspecionar uma branch antes de uma versão. Este handler parece inofensivo:
branch = request["branch"]
command = f"git show {branch}"
subprocess.run(command, shell=True, check=True)
Se branch for release; id, o shell verá dois comandos. A primeira parte pede ao Git que mostre uma revisão. A segunda executa id. O mesmo erro aparece em JavaScript com exec, em Ruby com uma string passada a system, em Go com sh -c e em scripts de CI que colocam a saída do agente diretamente em uma variável de shell sem controlar seu uso posterior.
O caractere perigoso não precisa chegar no prompt do agente. Ele pode estar no título de um pull request, em uma mensagem de erro, em um manifesto de pacote ou em um nome de arquivo retornado por uma varredura do repositório. O agente pode citar esse material em uma chamada de ferramenta posterior porque a tarefa manda inspecioná-lo ou corrigi-lo. Uma revisão que pergunta apenas se o modelo obedece a uma instrução ignora o ponto em que o texto se transforma em sintaxe executável.
A POSIX Shell Command Language descreve uma sequência que inclui expansões, divisão em campos quando aplicável, expansão de nomes de caminhos e remoção de aspas. Essa ordem importa. Um desenvolvedor costuma dizer «coloquei entre aspas», como se as aspas criassem uma string genérica e segura. Não criam. Elas restringem regras específicas de análise, em um shell específico e em um ponto específico da sequência de expansão.
Um shell também pode receber entrada de várias formas. Um valor pode afetar o próprio texto do comando, uma atribuição de ambiente, um alvo de redirecionamento, uma substituição de comando, um arquivo de inicialização do shell ou um código avaliado posteriormente por outro interpretador. Escapar para o primeiro shell não protege automaticamente o próximo analisador.
Mantenha esta distinção clara:
- Injeção de comandos ocorre quando a entrada altera a sintaxe interpretada por uma linguagem de comandos.
- Injeção de argumentos ocorre quando a entrada continua sendo um único argumento, mas muda a forma como o programa chamado interpreta esse argumento, muitas vezes como uma opção ou expressão.
- Falha de autorização ocorre quando um argumento válido solicita uma ação fora do escopo permitido ao chamador.
Usar um vetor de argumentos interrompe a primeira classe. Não resolve a segunda nem a terceira. Um caminho que escapa do workspace, uma revisão do Git que seleciona um objeto inesperado e uma URL que alcança um serviço interno podem ser argumentos perfeitamente separados.
Uma string de comando entre aspas ainda passa por analisadores demais
Colocar a entrada entre aspas antes de concatená-la a um comando de shell é popular porque parece uma correção pequena. Também é frágil, pois o comando costuma atravessar mais de uma gramática.
Suponha que um programa local construa uma chamada SSH e que o sistema remoto execute um shell. Um desenvolvedor pode colocar a branch entre aspas para o shell local, repeti-las para o SSH e colocá-las entre aspas uma terceira vez para o shell remoto. Cada camada tem pressupostos diferentes sobre espaços, aspas, barras invertidas, substituição de comandos e codificação. Uma mudança posterior pode remover uma camada de aspas ou colocar a string já protegida em um novo contexto. O código ainda parece cuidadoso, mas a proteção deixou de funcionar.
Este padrão é especialmente ruim:
const command = `git checkout '${branch}'`;
execFile("ssh", [host, command], callback);
execFile protege a chamada local do ssh, o que é bom. Ele não torna command seguro no lado remoto. O SSH normalmente fornece ao lado remoto uma string de comando. A conta remota costuma entregar essa string a um shell. Assim, a branch entra na sintaxe do shell um salto depois.
Aspas simples não são uma resposta geral. Uma entrada que contenha uma aspa simples pode encerrar a região protegida. Uma rotina feita para o shell POSIX está errada para o PowerShell. Uma rotina correta para uma palavra do shell está errada quando o valor cai em um redirecionamento, uma expressão aritmética ou um interpretador de linguagem. Uma rotina correta hoje ainda será um problema de manutenção quando alguém trocar git show por git log --format=... e reorganizar o template.
Não confunda serializar uma lista de argumentos para fins de log com reconstruí-la para execução. Uma linha como git show release-42 é útil para uma pessoa, mas não prova onde estavam os limites originais dos argumentos. Ela não é uma representação executável segura. Armazene os argumentos como um array estruturado e só os renderize para exibição com regras claras de escape.
Variáveis de ambiente criam outra armadilha. Isto é mais seguro que a interpolação:
BRANCH="$branch" git show "$BRANCH"
mas só continua seguro se o script controlar as duas expansões entre aspas e nunca avaliar o valor depois. Se um script posterior usar eval, construir outra string de comando ou passar o valor a um mecanismo de templates com recursos de execução, o trabalho inicial terá ajudado pouco. Um limite melhor evita entregar o valor do agente a um shell.
Executáveis fixos e arrays de argumentos removem a gramática do shell
Para ações locais, chame um executável fixo com um array de argumentos e deixe a execução do shell desativada. O sistema operacional entregará ao processo filho as strings individuais fornecidas. Ele não reinterpretará ponto e vírgula, cifrões, espaços, parênteses ou caracteres curinga como linguagem de shell.
A documentação de subprocess do Python recomenda uma sequência de argumentos e informa que shell=False é o padrão. Use esse padrão deliberadamente, em vez de depender dele por acaso:
import subprocess
result = subprocess.run(
["git", "status", "--porcelain=v1"],
cwd=repo_dir,
text=True,
capture_output=True,
check=True,
)
print(result.stdout)
Esta ação não tem executável controlado pelo agente nem template de comando controlado pelo agente. Ela executa uma operação conhecida em um diretório aprovado. A saída é texto simples, com cada registro de status em sua própria linha. Você pode melhorar a interface novamente analisando essa saída e retornando registros estruturados, em vez de enviar texto bruto para outra decisão do agente.
Quando uma operação precisa de entrada, mantenha o executável e a operação fixos e acrescente os valores validados como entradas separadas:
subprocess.run(
["tool", "fetch-resource", resource_id],
cwd=workspace,
check=True,
shell=False,
)
Em Node.js, prefira spawn ou execFile com um array de argumentos. Não ative a opção shell só para tornar conveniente um comando composto. Em Go, use exec.Command com cada argumento fornecido separadamente. Em Rust, use Command e chamadas repetidas a arg. As APIs são diferentes, mas a regra é a mesma: nenhuma entrada atravessa para uma linguagem de comandos.
Este é um bom momento para contestar uma recomendação comum: «Use um script de shell como wrapper seguro». Um script fixo pode ser uma fronteira aceitável de compatibilidade, mas não é mais seguro apenas por ser um script. Ele só será seguro quando receber valores por parâmetros posicionais fixos ou pela entrada padrão, colocar todas as expansões entre aspas, evitar eval e não construir outra string de comando. Um programa pequeno escrito com uma API de processos costuma ser mais fácil de auditar.
Há recursos legítimos de shell: pipelines, fluxo condicional, redirecionamento e built-ins. Coloque esses recursos em um script estático revisado quando não for possível removê-los. Não deixe o agente montar o pipeline. Dê a ele uma ação chamada collect_build_logs e deixe o wrapper executar seu pipeline fixo, com locais de arquivo fixos e parâmetros limitados.
O próprio executável também faz parte da política. Nunca o escolha com base na entrada do agente, em um arquivo de configuração gravável ou procurando em um PATH controlado por um atacante. Use um caminho absoluto controlado pelo administrador quando o ambiente exigir essa garantia. Defina um diretório de trabalho conhecido e um ambiente reduzido, em vez de herdar variáveis arbitrárias do processo do agente.
A validação define o significado permitido da ação
Arrays de argumentos protegem o limite do analisador. A validação protege o limite da ação. Se uma ação disser read_file, o serviço precisa decidir quais arquivos podem ser lidos por ela. Uma API de processos não toma essa decisão por você.
Comece com um esquema restrito. Um nome de implantação pode aceitar letras minúsculas, dígitos e separadores internos únicos, com limite de tamanho. Um seletor de branch pode ser um nome de branch dentro de um subconjunto escolhido das regras do Git. Um identificador de recurso pode ser um UUID ou um número do inventário. Um alvo de servidor pode ser um ID que o serviço converte em um host armazenado, em vez de um hostname arbitrário.
Evite listas de bloqueio genéricas como «rejeite ponto e vírgula e e comercial». Elas são criadas em torno de um analisador e deixam o abuso semântico intacto. Também crescem até que usuários comuns não consigam prever o que funciona. Uma gramática de permitidos oferece um contrato ao chamador e um conjunto limitado de entradas para o revisor analisar.
Para um campo de branch em um fluxo de release, uma gramática deliberadamente limitada pode ser melhor que aceitar toda referência permitida pelo Git:
import re
BRANCH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/]{0,127}")
def release_ref(value: str) -> str:
if not isinstance(value, str):
raise ValueError("branch must be text")
if not BRANCH.fullmatch(value):
raise ValueError("branch has unsupported characters")
if "/./" in value or "//" in value or value.endswith("/"):
raise ValueError("branch has an unsupported path form")
return "refs/heads/" + value
Este código não afirma implementar toda a gramática de referências do Git. Ele define intencionalmente uma gramática menor para uma ação de release. Essa costuma ser a escolha correta de engenharia. Se os usuários precisarem de espaços ou de outra forma incomum, acrescente-a porque o fluxo precisa dela, teste-a e documente-a. Não herde todos os casos extremos de um sistema de controle de versão geral quando sua ação só implanta branches de release.
O valor retornado tem um prefixo de namespace fixo. Isso importa. Uma expressão de revisão arbitrária pode apontar para tags, ancestrais de commits, reflogs ou uma sintaxe que o Git interpreta de maneira especial. Uma ação de branch de release não deve se transformar silenciosamente em «mostrar qualquer objeto do Git que o agente consiga descrever». Use uma biblioteca ou o modo de validação de branches do próprio Git quando precisar de compatibilidade completa e, ainda assim, mapeie o resultado aceito para o namespace permitido.
Nomes de recursos exigem a mesma disciplina. Se um agente solicitar um objeto de nuvem, mantenha a conta, a região, o bucket ou o projeto permitido fora do campo livre. Deixe que ele forneça um nome de objeto que siga a única gramática aceita. Não aceite uma URL completa e a chame de nome de recurso. Uma URL completa escolhe protocolo, host, porta, caminho e, às vezes, credenciais. Isso é uma decisão de autorização de rede disfarçada de string.
Limites de tamanho fazem parte da validação. Eles limitam o crescimento dos logs, evitam atingir limites acidentais da linha de comando e tornam a negação de serviço menos conveniente. As regras de codificação de caracteres também fazem parte dela. Rejeite caracteres de controle em campos de texto, a menos que a ação realmente precise deles. Rejeite NUL incondicionalmente em argumentos de processos, pois as interfaces de processos do sistema operacional não conseguem transportá-lo como parte de um argumento.
Caminhos exigem verificações de contenção, não filtros de caracteres
Um caminho pode ser seguro para o shell e perigoso para o sistema de arquivos. ../../secrets.env não contém nenhum metacaractere que as funções de aspas costumam considerar. Ainda assim, pode levar uma operação de arquivo para fora do workspace.
Uma política segura de caminhos começa declarando um diretório-raiz para a ação. Resolva o caminho relativo solicitado em relação a essa raiz e exija que o alvo resolvido permaneça dentro da raiz resolvida. Não aceite um caminho absoluto quando a ação só precisa de arquivos do workspace. Não normalize uma string e presuma que a grafia resultante informa para onde o sistema de arquivos irá.
Uma verificação conceitual se parece com isto:
from pathlib import Path
workspace = Path("/srv/agent-workspaces/repo-a").resolve()
def permitted_file(relative_name: str) -> Path:
if not isinstance(relative_name, str) or relative_name.startswith("/"):
raise ValueError("file must be a relative path")
candidate = (workspace / relative_name).resolve(strict=True)
if candidate == workspace or workspace not in candidate.parents:
raise ValueError("file is outside the workspace")
if not candidate.is_file():
raise ValueError("requested target is not a regular file")
return candidate
A chamada resolve detecta travessias comuns e segue links simbólicos existentes, o que é melhor que verificar se a string bruta contém ... Ainda assim, ela não é a resposta completa para uma operação de escrita sensível. Um atacante que consiga modificar a árvore de diretórios pode substituir um componente do caminho por um link simbólico depois da verificação e antes da abertura posterior. Essa lacuna entre verificar e usar é uma condição de corrida do tipo time of check to time of use.
Para operações de leitura em um workspace controlado, uma verificação de contenção após a resolução pode corresponder ao seu risco. Para escritas, exclusões, alterações de permissões ou arquivos compactados que possam ser influenciados por atacantes, use operações do sistema de arquivos que mantenham descritores de diretório e recusem a travessia de links simbólicos quando o sistema operacional oferecer esses controles. Mantenha workspaces graváveis pelos agentes separados do código que decide as raízes permitidas. Se o modelo de ameaça incluir um atacante local capaz de disputar a árvore de arquivos, uma biblioteca de caminhos de alto nível não basta.
Decida também se arquivos ocultos, metadados do repositório, diretórios de dependências geradas e links simbólicos pertencem ao escopo. Uma regra vaga como «qualquer coisa dentro do repositório» costuma expor arquivos de credenciais, caches de build ou configurações de que o fluxo nunca precisou. A permissão de leitura deve seguir o objetivo da ação, não a conveniência de um glob recursivo.
A extração de arquivos compactados merece um alerta separado. Valide cada membro depois de juntá-lo à raiz de destino e trate links dentro do arquivo como potencialmente hostis. A travessia de caminhos não se limita a solicitações feitas diretamente pelo agente. Uma ferramenta que extrai um artefato escolhido pelo agente pode realizar a mesma fuga em nome dele.
Opções e expressões continuam perigosas sem um shell
Um vetor de argumentos mantém um valor separado da sintaxe do shell, mas o programa chamado ainda analisa esse valor. É comum que programas tratem argumentos iniciados por hífen como opções. Alguns aceitam expressões, referências de configuração, sintaxe de inclusão de arquivos, plugins ou hooks de comando. Passar uma entrada não confiável como um único argumento não torna esses significados inofensivos.
Considere um wrapper que chama um programa de busca com um padrão fornecido pelo usuário. O wrapper pode fornecer corretamente ["search", pattern, directory]. Se o programa tratar o padrão como expressão regular, uma expressão patológica poderá consumir uma quantidade enorme de CPU. Se ele aceitar uma opção que lê um arquivo de configuração e o padrão cair na posição errada, o chamador poderá alterar o comportamento do programa. Se a linguagem do programa oferecer avaliação de código, você terá entregue ao agente outro interpretador.
Coloque as opções fixas antes dos dados e use o marcador convencional de fim das opções quando o programa oferecer esse recurso. Em texto, esse marcador são dois caracteres de hífen. Ele informa a muitos analisadores de linha de comando que os valores seguintes são operandos, não flags. Não dependa dele como validação. Algumas ferramentas não o respeitam, e um operando ainda pode ser um valor semanticamente perigoso.
O Git tem uma superfície de comandos ampla, por isso os agentes precisam de ações Git restritas, não de uma saída genérica «executar Git». Uma ação show_release_branch pode aceitar um nome de branch limitado, convertê-lo em refs/heads/ mais esse nome e chamar um subcomando Git fixo. Já uma ação checkout_anything, que aceita sintaxe de revisão arbitrária, tem um significado mais amplo e exige uma decisão de autorização mais ampla. São produtos diferentes, mesmo que ambos iniciem um processo Git.
A mesma regra vale para gerenciadores de pacotes, clientes de banco de dados, ferramentas de mídia, compactadores e comandos de infraestrutura. Pergunte qual gramática o programa de destino analisará depois que o sistema operacional entregar o argumento. Uma linha de comando costuma ser apenas o primeiro analisador de uma cadeia.
Expressões regulares exigem atenção especial. Um padrão não é código de shell, mas é trabalho executável dentro de um mecanismo de regex. Se o agente precisar pesquisar texto, escolha, quando possível, um mecanismo com desempenho previsível, imponha limites ao padrão e à entrada e ofereça a pesquisa literal como ação padrão. Não transforme toda solicitação de pesquisa em uma linguagem de expressões irrestrita só porque um desenvolvedor quer um endpoint flexível.
SSH transforma um comando local em um problema de análise remota
O SSH cria um segundo limite de execução. Você pode chamar o cliente SSH local com um vetor de argumentos perfeito e ainda enviar uma string de comando insegura ao host remoto. Muitas implementações de SSH executam o comando remoto solicitado por meio do shell da conta remota ou de um analisador de comandos equivalente.
Não construa comandos remotos a partir de campos do agente. Isto está errado mesmo quando a chamada local usa um array:
remote = "deploy " + environment + " " + branch
subprocess.run(["ssh", host, remote], check=True)
Uma organização segura envia um comando remoto constante e transporta os dados por um canal estruturado. Por exemplo, a conta remota pode expor um único programa revisado em um caminho fixo. O lado local inicia esse programa, envia uma solicitação JSON pela entrada padrão e o programa remoto valida environment e branch antes de executar qualquer operação local.
import json
import subprocess
request = {"environment": "staging", "branch": "release/42"}
subprocess.run(
["ssh", "deploy.internal", "/usr/local/libexec/receive-deploy-request"],
input=json.dumps(request) + "\n",
text=True,
check=True,
)
O comando remoto neste exemplo é constante. O JSON é um dado na entrada padrão, não um texto incorporado ao comando remoto. O receptor remoto ainda precisa rejeitar campos desconhecidos, impor limites de tamanho, validar cada valor e usar um vetor de argumentos em qualquer processo filho. O transporte estruturado elimina um labirinto de aspas. Ele não concede confiança.
O hostname normalmente deve ser escolhido de uma configuração armazenada por meio de um ID de ambiente opaco. Permitir que um agente forneça host significa deixá-lo escolher para onde as credenciais irão e qual limite de rede a ação atravessará. Se um fluxo realmente precisar de vários hosts, mapeie nomes aprovados, como staging e production, para configurações de conexão conhecidas. Mantenha a verificação do host ativada. Uma ferramenta que desliga essa verificação para evitar dificuldades de configuração removeu uma parte importante do limite.
Não envie credenciais no comando remoto, na linha de comando ou na solicitação JSON. Use o mecanismo de autenticação existente da conta remota e limite essa conta à ação que ela precisa executar. Um receptor de implantação que só consegue implantar uma aplicação é mais fácil de analisar que um agente que recebe uma sessão de shell ampla.
Aprovação não corrige um desenho inseguro de ação
A aprovação humana e uma trilha de auditoria são controles úteis, mas respondem a perguntas diferentes da validação. A aprovação responde se este chamador pode tentar uma ação agora. A validação responde se a solicitação tem uma forma definida e permitida. O log responde o que aconteceu e se o registro mudou depois. Tratar qualquer um desses controles como substituto dos outros produz ferramentas fracas com telas atraentes.
Uma aprovação de sessão única pode ser adequada para um gateway de ações fixas. Ela não é uma revisão cuidadosa de cada string interpolada que um processo alimentará em um shell durante o restante da sessão. A aprovação por chamada oferece mais oportunidades para uma pessoa notar um alvo estranho, mas ninguém deve precisar auditar regras de aspas aninhadas ou identificar um caractere Unicode visualmente parecido em um diálogo carregado.
Faça o cartão de aprovação mostrar a solicitação semântica: deploy branch release/42 to staging, não um comando de shell reconstruído a partir de um template. O receptor deve conseguir comparar o ambiente e a branch exibidos com o esquema da solicitação. Se um comando precisar de um campo de script bruto para se explicar, a ação é ampla demais para uma aprovação confiável.
Registre fatos estruturados no limite da ação. Guarde a identidade do chamador ou do processo do agente, o nome da ação aprovada, os argumentos ou campos de solicitação separados, o alvo escolhido, os horários, o status de saída e referências à saída, de acordo com suas regras de retenção de dados. Registre também as falhas de validação. Strings repetidas de travessia rejeitadas ou valores semelhantes a opções podem revelar um prompt ruim, um erro de integração ou uma sondagem ativa.
Para equipes que usam o Sallyport, a autorização por sessão, a aprovação de chave por chamada e os diários Sessions e Activity separados podem preservar controle e evidências em torno das ações dos agentes, mas o template de comando ainda precisa seguir as regras de projeto acima.
Um log com evidência de adulteração ajuda depois de um incidente apenas se registrar os limites que importavam. Uma string como ssh deploy internal deploy staging release 42 é ambígua depois do fato. Um evento com identidade separada do host, caminho fixo do receptor, campos JSON, resultado da autorização e status de saída do receptor pode apoiar uma investigação.
Teste o caminho de rejeição com a mesma seriedade do caminho feliz
A maioria dos testes de injeção de comandos prova apenas que uma branch normal, como release/42, funciona. Essa é a entrada menos interessante. A suíte precisa provar que o handler rejeita valores perigosos antes de iniciar um processo filho ou abrir um caminho.
Monte uma tabela pequena para cada tipo de campo. As entradas exatas dependem da gramática, mas as categorias devem incluir separadores de shell, substituições de comando, caracteres de aspas, espaços e quebras de linha, prefixos semelhantes a opções, componentes de travessia, valores vazios, caracteres de controle, valores grandes demais e sintaxe válida que solicite algo fora do escopo da ação. Para uma ação de branch, teste também um nome semelhante a tag e uma expressão de revisão que a interface não aceite.
Use um executor de processos falso nos testes unitários. Verifique o executável e cada argumento como valores separados. Verifique que uma entrada inválida nunca chama o executor. Um teste que compara apenas uma string de comando renderizada pode ignorar exatamente o limite que você está tentando proteger.
class RecordingRunner:
def __init__(self):
self.calls = []
def run(self, argv, **kwargs):
self.calls.append((argv, kwargs))
def test_rejects_branch_separator():
runner = RecordingRunner()
try:
release_ref("release/42; id")
except ValueError:
pass
else:
raise AssertionError("expected rejection")
assert runner.calls == []
Esse teste verifica a função de política, não o shell. Acrescente também um teste de integração para o limite do processo. Execute um programa de fixture inofensivo que imprima os argumentos recebidos com índices e tamanhos explícitos. Forneça entradas com espaços, aspas, caracteres curinga, parênteses e quebras de linha. Confirme que cada valor chega como um único argumento e que nenhum segundo programa é iniciado.
O fuzzing é útil depois que a ação tem um esquema. Gere Unicode aleatório dentro e fora do conjunto aceito e exija um de dois resultados: o validador rejeita a entrada ou a operação fixa a recebe como um único campo limitado. Não faça fuzzing contra um alvo de produção. O objetivo é encontrar pressupostos sobre analisadores no gateway e no receptor antes que o agente alcance um ambiente real com credenciais.
Por fim, revise todos os pontos que transformam uma entrada estruturada novamente em texto. Isso inclui visualizadores de logs com comandos clicáveis, snippets de shell gerados, configurações de CI, relatórios de erro, notificações de chat e wrappers remotos. Bugs de injeção costumam voltar quando uma solicitação estruturada e segura se transforma em uma string conveniente em um componente posterior.
A primeira ação que vale a pena corrigir costuma ser a mais flexível. Substitua run_command(command) por uma operação com nome, um pequeno esquema de solicitação, um executável fixo e uma suíte de testes cheia de entradas que ninguém digitaria durante uma demonstração normal. Essa mudança remove uma classe inteira de comportamentos do modelo do seu modelo de ameaças e deixa você com regras que realmente podem ser inspecionadas.
FAQ
Usar aspas no shell basta para evitar injeção de comandos?
As aspas mudam apenas a forma como um shell interpreta um valor. Elas não definem se esse valor representa um arquivo, branch, host ou recurso de API permitido. Trate as aspas como uma proteção do analisador e a validação como uma definição de permissões. Você precisa das duas.
Um agente de IA deve poder usar sh -c?
Em geral, não. Use uma API de processos que aceite um executável e um vetor de argumentos, com a execução do shell desativada. Isso remove a gramática do shell, mas você ainda precisa validar os argumentos de acordo com a semântica do programa chamado.
Um caminho aparentemente seguro ainda pode causar danos?
Um caminho como ../../private/config não contém metacaracteres do shell, mas ainda pode escapar do diretório pretendido. Resolva o caminho em relação a uma raiz aprovada, verifique a contenção depois da resolução e considere links simbólicos antes de abrir o arquivo ou agir sobre ele.
Vetores de argumentos são seguros quando o comando é executado por SSH?
Não. Um argumento separado continua sendo apenas um dado para o processo local, mas o SSH normalmente envia uma string de comando para ser interpretada por um shell remoto. Envie um comando remoto constante e passe a entrada estruturada pela entrada padrão. Depois, valide-a novamente no host remoto.
Como devo validar uma branch do Git fornecida por um agente?
Rejeite nomes fora da gramática realmente aceita pelo seu fluxo de trabalho e converta o nome aprovado em um namespace de referência fixo, como refs/heads/ seguido do nome. Não aceite revisões arbitrárias, expressões de referência ou opções de comando apenas porque o Git consegue interpretá-las.
Por que a injeção indireta de prompts importa para comandos do shell?
A injeção de prompt não precisa parecer maliciosa. Um arquivo do repositório, uma descrição de issue, um log de build ou uma resposta de API pode instruir o agente a passar um valor perigoso para uma ferramenta. O limite da ferramenta precisa aplicar suas próprias regras, mesmo quando o agente segue fielmente as instruções recebidas.
Diálogos de aprovação impedem comandos inseguros de agentes?
Uma aprovação informa que um processo pode fazer uma chamada. Ela não transforma um modelo de comando inseguro em seguro nem prova que o argumento está dentro do escopo. Projete a ação com segurança antes de pedir que alguém a aprove.
O que um log de auditoria deve registrar para um comando de agente?
Um registro útil inclui o executável, cada argumento em um campo separado, a identidade de quem chamou, o contexto do alvo, o resultado e um resumo criptográfico ou uma cópia protegida da entrada estruturada. Uma única linha de comando reconstruída perde as informações de limite necessárias para uma investigação.
Devo bloquear caracteres especiais ou usar uma lista de permitidos?
Rejeitar todos os caracteres incomuns só é seguro se o produto puder trabalhar com uma gramática restrita. Prefira uma lista de permitidos que corresponda ao domínio real, como nomes simples de implantações ou branches de repositórios, e documente os formatos aceitos. Não finja que uma lista de bloqueio genérica entende todos os analisadores envolvidos.
Como testar um gateway de ações de agente contra injeção de comandos?
Comece com uma ferramenta que aceite uma operação limitada, uma solicitação estruturada e nenhum campo de comando do shell. Teste separadores, substituições, caracteres de opção no início, caminhos de travessia, quebras de linha e valores que sejam sintaticamente válidos, mas estejam fora do escopo. Se algum caso alcançar uma ação não pretendida, a interface é ampla demais.