Codificação na fronteira de saída
Trate tudo o que o modelo emite como entrada hostil para quem o consome. Codifique e valide em cada destino — renderizador, shell, consulta, agente a jusante — com as regras desse destino. Um sanitizador global não dá conta: o escape correto para HTML não significa nada para um shell.
Definição
A codificação na fronteira de saída é a prática de aplicar codificação e validação específicas de cada destino à saída do modelo em todo ponto onde ela cruza para um sistema que vai interpretá-la, em vez de filtrá-la uma única vez, de forma genérica, ao sair do modelo. Sua ausência é a fraqueza que a OWASP chama de improper output handling: tratamento indevido da saída.
Problema
As equipes endurecem o lado da entrada contra injeção de prompt e deixam o lado da saída aberto. O texto do modelo então chega a um renderizador, a um shell, a um banco de dados ou ao contexto de outro agente, onde é interpretado como instrução em vez de exibido como dado. O atacante nunca precisa alcançar o modelo diretamente. Dito de outro modo: a saída do modelo tem de ser tratada como entrada não confiável.
Quando usar
Qualquer agente cuja saída chegue a algo que a analisa: uma interface de chat que renderiza markdown, um agente de código que executa um comando sugerido, uma chamada de ferramenta construída com argumentos gerados, um resumo injetado no prompt de um segundo agente, ou um webhook que encaminha o texto adiante.
Solução
Enumere os destinos antes de escrever qualquer filtro. Cada lugar onde a saída do modelo aterrissa e é interpretada — renderizador HTML, shell, consulta, caminho de arquivo, buscador de URLs, contexto de outro agente, webhook a jusante — é uma fronteira distinta com regras distintas.
Codifique no destino, não na origem. Escape HTML para o renderizador, parametrize a consulta, passe um array argv ao processo. A codificação pertence ao lugar onde ocorre a interpretação, porque só ali se sabe o que será interpretado.
Prefira saída estruturada a prosa que depois é preciso analisar de volta. Uma chamada de ferramenta com argumentos tipados tem um esquema contra o qual validar; uma frase da qual você extrai um nome de arquivo com expressão regular, não.
Valide o valor, não só a sintaxe. Um caminho codificado continua sendo um caminho: verifique que ele resolve dentro do diretório pretendido antes de abri-lo.
Trate as URLs de saída como um destino próprio. Imagens e links renderizados provocam uma requisição sem clique, portanto levam dados para fora; permita apenas os hosts que um link renderizado pode alcançar.
Faça da fronteira o único caminho. Se alguma rota de código puder consumir saída crua do modelo sem passar por um codificador de destino, o controle é orientativo, não real.
Componentes
Benefícios
- Rompe a cadeia de injeção onde importa: nem um modelo completamente persuadido consegue fazer um sistema a jusante agir, porque esse sistema nunca interpreta o texto dele.
- Independente do comportamento do modelo. Continua funcionando entre versões, novos jailbreaks e mudanças de prompt, porque não depende de o modelo recusar nada.
- Verificável. Cada destino tem um payload e um resultado de passa ou falha, então o controle produz evidência em vez de garantia.
- Barato quando aplicado cedo. Adicionar um codificador é uma mudança de fronteira; colocá-lo depois que o destino já está em toda parte é uma refatoração.
Riscos
- Um único sanitizador para todos os destinos. Parece um controle, satisfaz a lista de verificação e está errado em todas as fronteiras menos naquela para a qual foi escrito.
- Codificação que quebra o produto: escapar demais transforma markdown legítimo, blocos de código e texto não latino em ruído, e a pressão por afrouxar recai sobre o codificador em vez de sobre a lista de destinos.
- Um inventário de destinos que envelhece. Cada nova integração acrescenta consumidores, e nada falha quando um é esquecido.
- Confundir detecção com codificação. Varrer a saída em busca de cadeias suspeitas pega os payloads do ano passado; a codificação não precisa reconhecer o ataque.
Quando não usar
- Saída que nunca é interpretada: uma pontuação, um enum, um booleano que o chamador compara. Restrinja o tipo em vez disso; um codificador sobre um conjunto fechado de valores é cerimônia.
- Ferramentas locais de um só usuário, sem renderização e sem execução de processos, onde o único consumidor é uma pessoa lendo texto.
- Onde o destino já parametriza por construção, como o binding de um ORM ou um motor de templates que escapa por padrão. Um segundo codificador não acrescenta nada e pode causar dupla codificação.
Tecnologias
Exemplos
- Um agente de suporte resume um chamado cujo corpo contém uma imagem markdown apontando para o host de um atacante. O console a renderiza, o navegador busca a URL, e a conversa vaza sem que ninguém clique em nada. Desabilitar HTML cru e permitir apenas certos hosts de imagem fecha isso.
- Um agente de código propõe um comando de shell. O executor passa a string a um shell, então um nome de arquivo com um separador de comandos é executado. Passar um array argv elimina por completo a etapa de análise do shell.
- O resumo de um agente é colocado no prompt de um segundo agente. O resumo continha instruções e o segundo as seguiu. Delimitar o trecho não confiável e rotulá-lo como dado é a fronteira nesse caso.
KPIs
- Cobertura de destinos
- Proporção de consumidores conhecidos da saída do modelo com codificador na fronteira. Abaixo de 100% o controle tem um buraco concreto, e nomear o destino serve mais do que uma porcentagem que o dilui na média.
- Taxa de neutralização de payloads
- Proporção de payloads de teste por destino neutralizados na fronteira. O alvo é 100%: qualquer outro número nomeia um destino a corrigir, não um número a melhorar.
- Tempo até cobrir um novo destino
- Quanto tempo passa desde que uma integração entra em produção até que o codificador dela exista. Mede se o inventário acompanha o produto, não se ele acertou uma vez.
- Volume de disparos na fronteira
- Com que frequência os codificadores neutralizam algo em produção. Um zero constante costuma significar que o codificador não está no caminho, não que nada hostil esteja chegando.
Modos de falha observados
- Exfiltração silenciosa por marcação renderizada: uma imagem ou um link provocam uma requisição sem clique, então os dados saem sem ação do usuário e sem nenhum erro que alguém fosse notar.
- Injeção de segunda ordem: a saída é codificada corretamente para o console, é armazenada e depois renderizada em outro lugar — um visualizador de logs, um chamado, um e-mail de resumo — onde essa codificação não se aplica.
- O codificador que só está no caminho feliz. Ramos de erro, novas tentativas e fallbacks emitem o mesmo texto por outra rota de código, sem nada nela.
- Dupla codificação. Duas camadas escapam corretamente, os usuários veem entidades escapadas no produto, e a correção remove a camada errada.
Lições aprendidas
- Enumere destinos antes de escrever filtros. Quase toda falha real aqui é um consumidor que ninguém listou, não um codificador escrito errado.
- O destino que se esquece raramente é uma tela. É um webhook, um visualizador de logs, uma exportação ou um e-mail de resumo: algum lugar para onde a saída vai sem que ninguém o pense como renderização.
- A codificação vence a detecção porque não precisa reconhecer o ataque. Uma lista de bloqueio de payloads é uma descrição dos ataques que já eram públicos.
- Não deixe um único sanitize() virar a resposta. O nome sugere completude e o comportamento está correto para exatamente um destino.
FAQs
- Isso não é o mesmo que filtrar entradas contra injeção de prompt?
- Não: defendem extremos opostos. O filtro de entrada tenta impedir que o modelo seja persuadido, e isso depende do modelo. Isto aqui assume que a persuasão já ocorreu e impede que se aja sobre a saída, e isso não depende do modelo em nada. Um sistema que só tenha o primeiro falha assim que surge um novo jailbreak.
- Isto não é simplesmente sanitization da saída, com um sanitizador único para tudo?
- Porque a codificação é contextual. Escapar uma aspa protege um renderizador HTML e não faz nada por um shell; aspas de shell protegem o shell e corrompem o texto exibido. Uma função compartilhada precisa escolher um contexto, está errada nos demais, e parece cobertura enquanto é um ponto único de falha.
- O modelo é nosso e o prompt é fixo. Ainda assim é preciso?
- Sim, se algo que o modelo lê vem de fora: uma página buscada, um arquivo do usuário, o resultado de uma ferramenta, um registro de memória. A instrução não precisa chegar pelo seu prompt: chega por aquilo que o modelo lê, e o seu prompt ser fixo não restringe isso.