---
title: "Quantas instruções escondidas chegam ao seu agente? Na minha demo, 8 de 8"
slug: texto-escondido-instrucoes-chegam-ao-agente
date: 2026-11-12
category: "Arquitetura"
tags: ["ia", "agentes", "prompt-injection", "seguranca", "typescript", "frontend"]
readTime: "4 min"
excerpt: "Escondi oito instruções numa página com CSS e HTML. A pessoa não vê nenhuma, o leitor do agente pega as oito. E mesmo lendo o texto renderizado com innerText no Chrome, cinco ainda passam. O que eu faria para defender um agente que lê a web."
url: https://eliseu.dev/blog/texto-escondido-instrucoes-chegam-ao-agente
project: hidden-text
---

Escondi oito instruções numa página de previsão do tempo. Na tela, nenhuma aparece. No texto que um leitor de agente extrai do HTML, aparecem as oito. E quando troquei a extração pelo texto renderizado do navegador, que parecia a correção óbvia, cinco continuaram passando.

Em outubro eu escrevi sobre [o texto que só a IA lê](/blog/texto-que-so-a-ia-le-prompt-injection), com casos reais. Desta vez quis medir.

## O que a demo faz

É o quinto item da série Frontend com IA: [05-hidden-text](https://github.com/EliseuSantos/frontend-ia-demos/tree/main/demos/05-hidden-text), em TypeScript estrito. Uma página de exemplo recebe oito técnicas, cada uma com uma instrução falsa e fácil de achar com grep: mesma cor do fundo, fonte de tamanho 0, `opacity: 0`, `display: none`, `aria-hidden="true"`, comentário HTML, posicionado fora da tela e `content` num `::after`. Um painel mostra o que a pessoa vê. O outro mostra o que o leitor do agente extrai. O placar conta quantas instruções o agente acha e a pessoa não.

Com as oito ligadas, o placar fica em 8 de 8. Dá para ligar uma por vez pela URL, por exemplo [só o `::after`](https://demos.eliseu.dev/frontend-ia-demos/05-hidden-text/?tecnica=after).

Veja: não tem modelo nenhum aqui. O leitor do agente é um extrator determinístico, parecido com o que muita ferramenta de "ler página" faz antes de mandar o texto para o LLM. Ele junta o texto sem tags, os comentários e o `content` do CSS (trecho de `src/extractor.ts`):

```typescript
const comments = extractComments(withoutScripts);
const withoutComments = withoutScripts.replace(COMMENT_RE, ' ');
const bodyText = stripTags(withoutComments);
const pseudo = extractStyleContents(`${withoutScripts}\n${css}`);
return [bodyText, ...comments, ...pseudo].join(' ');
```

Então a demo mostra o que chega ao agente. Ela não prova que um LLM obedeceria. Isso depende do modelo, do prompt e das ferramentas que ele tem na mão.

## Onde eu errei

Minha primeira reação foi: é só usar o texto renderizado. O `innerText` do navegador respeita o CSS, e o [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText) descreve exatamente isso.

Testei a mesma página da demo no Chrome 156 headless, lendo `document.body.innerText`. Caíram três técnicas: `display: none`, comentário e `::after`. Passaram cinco: cor igual ao fundo, fonte 0, opacidade 0, `aria-hidden` e fora da tela. Para o navegador, esse texto está renderizado. Só não dá para ver.

Tem mais um detalhe honesto. A extração "visível" da demo sabe quais classes escondem texto, porque fui eu que montei a página. Uma página de verdade não avisa. É como conferir a mala no raio-x sabendo onde você mesmo guardou o canivete.

## Como eu defenderia

Acho que filtrar o HTML ajuda, mas não fecha a porta. Eu montaria a defesa em camadas:

1. **Conteúdo da web entra como dado.** O texto extraído vai num bloco marcado como não confiável, separado das instruções do sistema. Isso reduz o risco, sem zerar. A [OWASP coloca prompt injection em primeiro lugar](https://genai.owasp.org/llmrisk/llm01-prompt-injection/) justamente por não ter correção completa.
2. **Ação com efeito precisa de outra porta.** Se o agente pode aprovar um pedido, essa ferramenta não pode depender só do que ele leu. Aprovação humana ou política deny-by-default, como discuti no post sobre [hooks de política](/blog/inference-hooks-claude-falha-aberta-ou-fechada).
3. **Logar o que chegou ao modelo.** Guardar o texto extraído, e não só a URL, é o que permite explicar depois por que o agente fez o que fez.
4. **Canário no teste.** O truque da demo serve para qualquer pipeline: instruções falsas e únicas, uma por técnica, e um teste que falha se alguma delas aparecer na entrada do modelo. A demo tem um teste por técnica em `src/extractor.test.ts`.

## O que muda para você

Se o seu agente lê páginas, PDFs ou e-mails, pegue a página da demo, passe pelo seu leitor e conte quantas instruções chegam. Meu palpite é que o número não vai ser zero. O código está no [repositório frontend-ia-demos](https://github.com/EliseuSantos/frontend-ia-demos), e as outras demos da série estão em [eliseu.dev/projects](https://eliseu.dev/projects).

Sou Tech Lead de IA e esse tipo de fronteira entre o que o agente lê e o que ele pode fazer é o que eu gosto de discutir. Se quiser conversar sobre o seu caso, [me chama aqui](/contact).

## Fontes

- [Código da demo 05-hidden-text](https://github.com/EliseuSantos/frontend-ia-demos/tree/main/demos/05-hidden-text), repositório [frontend-ia-demos](https://github.com/EliseuSantos/frontend-ia-demos)
- [Demo no ar](https://demos.eliseu.dev/frontend-ia-demos/05-hidden-text/), com `?tecnica=` ou `?tecnicas=` para escolher as técnicas
- [HTMLElement.innerText](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText), MDN
- [LLM01:2025 Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/), OWASP GenAI Security Project
- Medição do `innerText`: a página de exemplo da demo, com as oito técnicas, aberta no Chrome 156 headless em 12/11/2026