Pular para o conteúdo
11 min de leitura

Guardrails e validação de saída de LLM: nunca confie cegamente no que o modelo devolve

Compartilhar

Por Lucas Andrade ·

A saída de um LLM é entrada não confiável. Como validar, sanitizar e cercar o que o modelo devolve antes de deixar isso tocar seu banco, seu shell ou seu HTML.

Neste artigo

Existe uma cena que se repete em quase toda equipe que bota um modelo de linguagem em produção pela primeira vez. Alguém constrói um endpoint que recebe uma pergunta do usuário, monta um prompt, chama o modelo, pega a string de resposta e devolve para o front. Funciona lindamente na demo. Meses depois, um pentester manda um prompt que faz o modelo responder com <img src=x onerror=alert(document.cookie)> e descobre que aquela string caiu direto num innerHTML. Ou pior: alguém construiu um agente que traduz linguagem natural em SQL, o modelo devolveu DROP TABLE users; -- embutido numa consulta bem-educada, e o código executou sem pestanejar. O erro conceitual em todos esses casos é o mesmo, e é o assunto deste artigo: tratar a saída do LLM como se fosse dado confiável, gerado pela sua aplicação, quando na verdade ela é entrada não confiável exatamente como o corpo de uma requisição HTTP vinda da internet.

A saída do modelo é entrada, não código seu

O OWASP catalogou isso no seu Top 10 para aplicações com LLM sob o nome de Insecure Output Handling. A ideia central é desconfortável mas simples: no momento em que você concatena texto controlado pelo usuário dentro de um prompt, você perdeu o controle determinístico sobre o que sai do outro lado. Um LLM não é uma função pura que você auditou linha por linha; é um sistema estatístico que pode ser induzido, por engenharia de prompt direta ou por injeção indireta (um documento no RAG, um e-mail que o agente lê, uma página que ele navega), a produzir praticamente qualquer sequência de caracteres. Isso inclui payloads de XSS, comandos de shell, fragmentos de SQL, chamadas de ferramenta maliciosas e Markdown com links de exfiltração. A saída é, para todos os efeitos de segurança, um canal por onde o adversário fala com o resto do seu sistema.

Reformule mentalmente a arquitetura assim: o LLM é um usuário anônimo e potencialmente hostil sentado dentro do seu backend. Ele tem a boa vontade de ajudar na maioria das vezes, mas não tem noção de fronteiras de confiança, não distingue instrução de dado, e alguém do lado de fora pode estar ditando o que ele diz. Você não passaria o corpo cru de um formulário público direto para eval(). Pela mesma razão exata, você não passa a saída do modelo direto para lugar nenhum sensível. Toda a disciplina que a indústria construiu ao longo de vinte anos para lidar com entrada não confiável — validação, sanitização, encoding contextual, menor privilégio — vale integralmente aqui, só que muita gente esquece de aplicá-la porque a string "veio do nosso próprio modelo".

Validação estruturada antes de usar qualquer coisa

O primeiro anteparo é parar de consumir texto livre quando você precisa de dado estruturado. Se a sua aplicação espera que o modelo classifique um ticket em uma de cinco categorias, ela não deve aceitar um parágrafo em prosa e tentar adivinhar a intenção; ela deve exigir uma saída em formato verificável — tipicamente JSON — e então validar esse JSON contra um schema rígido antes de deixar qualquer valor circular. Ferramentas de saída estruturada, function calling e constrained decoding ajudam a aumentar a probabilidade de o modelo emitir a forma certa, mas note bem: elas aumentam a probabilidade, não garantem a verdade. O schema do seu lado continua sendo obrigatório porque é o único ponto determinístico da cadeia.

Validar contra schema significa mais do que checar se o JSON faz parse. Significa afirmar tipos (o campo quantidade é inteiro, não uma string que você vai coagir depois), afirmar faixas (a nota está entre 0 e 10), e — o mais importante para segurança — afirmar allowlists. Se a categoria só pode ser um de cinco valores conhecidos, qualquer sexto valor é rejeitado imediatamente, não sanitizado, não "aproximado": rejeitado. Um enum no seu validador é infinitamente mais confiável do que qualquer instrução no prompt pedindo "responda apenas com uma das categorias abaixo". O modelo pode ignorar a instrução; o enum não ignora nada. Essa é a regra que atravessa o artigo inteiro: validação determinística no seu código vale mais que pedir para o modelo se autopoliciar. Instrução em prompt é sugestão; código é lei.

Quando o dado sai bem-formado e dentro da allowlist, você o usa. Quando ele sai malformado, fora de faixa ou fora da allowlist, você tem um caminho de erro explícito: rejeitar, registrar com contexto, opcionalmente tentar de novo com um limite de tentativas, e nunca simplesmente "seguir com o que deu para aproveitar". Um caminho de erro engolido aqui é uma porta aberta.

Nunca passe a saída direto para um interpretador

Aqui mora a categoria mais perigosa, porque combina a imprevisibilidade do modelo com o poder de um interpretador. Existem quatro sumidouros clássicos onde a saída de um LLM causa estrago catastrófico, e o princípio é o mesmo em todos: entre o modelo e o interpretador, tem que haver uma camada de validação e escape.

O primeiro é a execução de código. Padrões de "agente que resolve problemas escrevendo Python" e ferramentas de análise de dados frequentemente pegam código gerado pelo modelo e o executam. Se esse código roda no mesmo processo, com as mesmas permissões e acesso à rede do seu backend, você acaba de dar Remote Code Execution a quem controlar o prompt. A resposta não é confiar mais no modelo; é isolar a execução: sandbox sem rede, sem acesso ao filesystem do host, com limite de CPU e memória, com timeout duro, num container descartável rodando como usuário sem privilégio. O interpretador continua sendo perigoso, mas o raio de explosão fica contido.

O segundo é o SQL. Se você deixa o modelo gerar consultas, ele vai eventualmente gerar algo destrutivo ou algo que vaza dados de outro tenant. A defesa aqui não é diferente da defesa contra SQLi clássico: o modelo nunca produz a query final que vai crua para o banco. Ou você restringe o modelo a preencher parâmetros de consultas parametrizadas que você escreveu, ou você roda o que ele gera contra uma conexão de banco read-only, com escopo de schema mínimo, e ainda assim valida a árvore sintática para recusar qualquer coisa que não seja um SELECT. Concatenar a saída do modelo dentro de uma string SQL é a versão de 2025 do mesmo erro que criou a injeção de SQL nos anos 90.

O terceiro é o shell. Agentes que executam comandos de sistema a partir de texto do modelo são uma fonte inesgotável de injeção de comando. Se em algum momento você monta uma string de shell interpolando saída do modelo, você já perdeu — metacaracteres como ;, |, $() e crase transformam qualquer comando aparentemente inócuo numa cadeia arbitrária. Passe argumentos como vetor, nunca como string interpretada pelo shell; restrinja a um conjunto fechado de comandos permitidos; e prefira APIs de biblioteca a invocar binários sempre que possível.

O quarto é o HTML, e é o mais comum porque quase todo produto renderiza a resposta do modelo numa interface. Saída de LLM colocada em innerHTML, dangerouslySetInnerHTML ou set:html sem sanitização é XSS servido em bandeja, com o agravante de que o payload pode ter sido plantado por injeção indireta num documento que o modelo apenas resumiu. A resposta é output encoding contextual no render: se é texto, ele entra como texto (o framework escapa por padrão — não desligue isso); se você realmente precisa renderizar Markdown ou HTML vindo do modelo, passe por um sanitizador com allowlist rígida, tipo DOMPurify, que remove <script>, atributos on*, javascript: em href e demais vetores. E cuidado com um vetor sutil: links e imagens em Markdown gerados pelo modelo podem apontar para um domínio do atacante com dados do usuário embutidos na URL, servindo de canal de exfiltração silencioso quando o navegador busca a imagem.

Guardrails de conteúdo: o que sai também precisa de política

Além dos sumidouros técnicos, existe uma classe de guardrails sobre o conteúdo da resposta, e ela protege o usuário e o negócio, não só o interpretador. Moderação de saída verifica se o modelo produziu conteúdo tóxico, ilegal ou fora de política antes de mostrá-lo — especialmente relevante quando a entrada foi adversarial. Filtragem de PII na saída impede que o modelo, tendo tido acesso a dados sensíveis no contexto (num RAG multi-tenant, por exemplo), vaze CPF, e-mail ou número de cartão de um usuário para outro. Detecção de vazamento de system prompt barra respostas onde o modelo, induzido, cospe as instruções internas, chaves ou regras de negócio que deveriam permanecer opacas.

E existe o guardrail de recusa, que é a contraparte legítima: em certos casos a resposta correta e segura é o modelo (ou a camada em volta dele) se recusar a responder. Detectar quando a saída deveria ter sido uma recusa e não foi — ou quando foi uma recusa mas o conteúdo perigoso vazou junto — faz parte da validação. Tudo isso é defesa determinística sobre a saída probabilística: você não confia que o modelo se comportou; você verifica o que ele produziu.

Agentes: confirmar, escopar, humano no laço

Quando o LLM não apenas fala mas age — chama ferramentas, envia e-mails, move dinheiro, apaga registros —, os guardrails de saída viram guardrails de comportamento, e a régua sobe. Três princípios são inegociáveis. Primeiro, confirmação antes de ação destrutiva ou irreversível: apagar dados, enviar comunicação externa, executar transação financeira nunca deve acontecer só porque o modelo decidiu; passa por uma checagem determinística e, quando o impacto é alto, por um humano no laço que aprova explicitamente. Segundo, escopo mínimo de ferramenta: cada ferramenta exposta ao agente tem permissões estreitas e verificadas do lado do servidor — o agente que precisa ler faturas não recebe a credencial que também apaga faturas. Isso é o mesmo excessive agency que o OWASP alerta: o dano de um agente comprometido é limitado pelo que suas ferramentas permitem, então limite-as. Terceiro, autorização re-verificada no servidor a cada chamada de ferramenta, com tenant_id e papel vindos do token verificado, nunca do que o modelo "disse" ser o usuário.

Defense-in-depth e como testar

Nenhum desses anteparos é suficiente sozinho, e é aí que entra a mentalidade de defesa em profundidade. Você valida a entrada (para reduzir injeção), valida e sanitiza a saída (para conter o que passou), e monitora o comportamento em produção com logs correlacionados por trace_id, alertas sobre picos de recusa, sobre saídas rejeitadas pelo schema e sobre chamadas de ferramenta anômalas. As três camadas se cobrem: quando a de entrada falha — e ela vai falhar, porque prompt injection não tem solução completa hoje —, a de saída ainda segura, e o monitoramento avisa que alguém está tentando.

Não esqueça o vetor de custo e disponibilidade. Geração é cara e lenta; um atacante que faz o modelo produzir saídas gigantescas em loop, ou que dispara milhares de gerações, comete um Denial of Wallet — esgota seu orçamento de tokens ou trava sua fila. Imponha limites de tamanho de saída, timeouts, rate limiting por usuário e tetos de custo por tenant, tratando geração como o recurso finito e abusável que ela é.

Por fim, teste como um atacante. Red-team da saída significa alimentar o sistema com prompts projetados para fazer o modelo emitir XSS, SQL, comandos de shell, PII de outro tenant e o próprio system prompt, e então verificar que a sua camada de validação rejeitou cada um — não que o modelo "se comportou bem", mas que o seu código barrou o payload independentemente do que o modelo fez. Inclua asserções negativas (o payload malicioso não apareceu no render), casos hostis (unicode, null byte, JSON malformado, valores fora da allowlist) e um teste que force uma falha conhecida para provar que o guardrail está de fato ativo e não silenciosamente desligado. Um guardrail que nunca reprovou nada nos testes provavelmente está cego.

Conclusão prática

A regra que resume tudo cabe numa frase: trate a saída do LLM como você trata a entrada de um usuário anônimo da internet — desconfie, valide, escape e limite o privilégio de tudo que ela toca. Exija estrutura verificável quando precisar de dado, valide contra schema com tipos e allowlists, jamais passe o texto cru para eval, SQL, shell ou HTML sem a camada de contenção apropriada, aplique guardrails de conteúdo e de comportamento, confirme ações destrutivas com humano no laço, e monte defesa em profundidade com monitoramento. O modelo é um colaborador brilhante e um vetor de ataque ao mesmo tempo; sua arquitetura é que decide qual dos dois vence. Coloque a validação determinística entre o modelo e todo interpretador sensível, e você transforma uma máquina imprevisível numa peça que você pode, finalmente, colocar em produção sem prender a respiração.

Compartilhar

Compartilhar

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly