DEV Community

Tiago Vilas Boas (Montanha)
Tiago Vilas Boas (Montanha)

Posted on

O prompt de AppSec que eu criei achou 4 gaps de segurança

Pessoal, eu quase mandei uma lista de 23 "vulnerabilidades" para projetos de software livre.

Esses dias rodei um prompt de AppSec próprio em alguns repositórios conhecidos.

XSS, CORS, CSP, um SVG suspeito. O LLM tinha encontrado um monte de coisa. Na hora, parecia um ótimo resultad, volume, evidência, todo trabalho feito.

Aí eu parei e pensei: se eu fosse o cara do outro lado da issue, eu levaria isso a sério?

A conta virou quando eu parei de pedir "audita meu código". Passei a escrever o contrato antes de abrir o repo. Direção (o que o modelo pode afirmar) e disciplina (o que ele não pode). Isso não deixa o LLM mais inteligente. Controla o comportamento. Em segurança, isso é o que separa auditoria de lista.

O que você leva daqui: o contrato (esqueleto), por que o scanner não substitui hunt, e as três portas pra não colar relatório no tracker. Os repos no meio são prova. Não são o ponto.

Tabela de Conteúdo

  • 1. Direção e disciplina não deixam o modelo mais inteligente
  • 2. O scanner vê o que está. O hunt vê o que falta
  • 3. Silêncio também é resultado
  • 4. As três portas
  • 5. O que você monta amanhã

1. Direção e disciplina não deixam o modelo mais inteligente

Esse é o ponto que eu demorei pra aceitar.

O modelo já conhece OWASP. O que muda é o que ele tem permissão de afirmar. Sem isso, "audita meu código" devolve XSS fantasma, CORS, CSP, um SVG. Volume. A pergunta de segurança some: disto aqui, o que vira issue, o que vai pro e-mail privado, o que é silêncio?

Direção amarra três coisas antes da leitura:

  1. Padrão externo. OWASP ASVS 5.0, com o ID do requisito. "Boas práticas" não conta.
  2. Invariante em uma frase, falsificável. "Webhooks deveriam ser autenticados" é opinião. "Todo handler de webhook de entrada verifica autenticidade do remetente antes de mutar estado" é hunt.
  3. Formato único. Finding, observação e hipótese não misturam.

Disciplina é o que corta o teatro. O texto que o modelo vê antes de abrir arquivo:

Padrão: OWASP ASVS 5.0, com o ID do requisito. "Boas práticas" não conta.
Invariante: uma frase, falsificável.
Relatório: finding, observação e hipótese não misturam.

Sem arquivo:linha → não é finding.
CWE inventado → encerra a sessão.
Needs-runtime → não vira card.
Sem X de Y → não é hunt.
Estático. Sem PoC. Sem request no ambiente deles.
A sessão que encontra não é a sessão que corrige.
Enter fullscreen mode Exit fullscreen mode

Esse último item parece burocracia até o modelo achar um gap e, empolgado, reescrever o where do Prisma no mesmo fôlego, sem teste. Finding de um lado. Fix de outro.

O prompt completo é privado. Por trás dele tem regras numa knowledge base (quando hunt, quando varredura, o que é finding, as três portas) e skills do harness que amarram o modelo nisso. Sem a KB o modelo improvisa. Sem a skill ele lê o bloco e segue viagem. O esqueleto acima é o que cabe num artigo.

Um limite honesto: isso só rende em modelo de raciocínio alto. Num modelo fraco, prompt bom organiza melhor o erro.

2. O scanner vê o que está. O hunt vê o que falta

Scanner estático (Semgrep, CodeQL, Bandit) acha padrão que existe. Chamada perigosa, query concatenada, header ausente. É rápido, determinístico, barato. Roda a cada push. Eu não troco isso por LLM.

O que ele estruturalmente não vê é um controle ausente. "Esta rota deveria checar o dono do recurso antes de retornar, e não checa" não é regex. É propriedade semântica: o que é um tenant, que recurso pertence a quem, qual rota é pública de propósito.

Por isso o Semgrep entra neste texto. Não porque ele tenha achado os gaps. Porque, no mesmo commit do caso das 174 rotas, ele foi o baseline de CI. Sete rulesets, 25 segundos:

semgrep --config p/default --config p/typescript --config p/react \
        --config p/nextjs --config p/owasp-top-ten \
        --config p/javascript --config p/secrets
Enter fullscreen mode Exit fullscreen mode

23 findings. 17 eram unsafe-formatstring em template. Zero de autorização.

Autorização quebrada (BOLA, OWASP API #1) não tem regra sintática estável: o padrão certo e o errado usam o mesmo findUnique.

O hunt começa invertido. Você escreve a invariante antes de abrir o arquivo. Enumera caminhos. Classifica: enforced-explicitly, inherited, gap, out-of-scope. No fim tem denominador. "Achei um IDOR" sem "1 de 174" não é hunt. É vibe.

Naquele commit: 173 de 174 rotas seguravam. Uma não. Esse é 1 dos 4 gaps de autorização da rodada. Os outros 3 estão num alvo cujo detalhe de rota não entra neste post. Formbricks e Dub são hardening, não entram nos 4.

Scanner Hunt dirigido
Enxerga padrão presente controle ausente
Bom em injeção, XSS, secret, header, crypto fraca o 1 caminho em 100 que pula o guard
Custo centavos, a cada PR dólares, periódico
Falso positivo ruído sintático alucinação de finding, se você soltar o denominador
Ponto cego lógica de negócio cobertura exaustiva do trivial

Duas cadências, duas perguntas. Verde no CI nunca respondeu "a invariante ainda vale".

3. Silêncio também é resultado

A falha mais comum de IA em segurança é fabricar achado para não devolver silêncio. Disciplina se mede nisso.

Busca ingênua por dangerouslySetInnerHTML em um app Next: oito hits, XSS crítico fantasma. Seguindo o dado até a função, é DOMPurify. Em SSR o React escapa. Zero finding. O que virou issue foi CORS contraditório (* + credentials), que o navegador rejeita. formbricks#9081. Tinha um SVG Needs-runtime. Não virou card. Prompt solto teria aberto dois. O dirigido abriu um.

Webhook de entrada: 7 de 7 autenticam o remetente. Semgrep no escopo: 0. Autenticidade não é padrão sintático. Dois HMAC com !== no mesmo repo que já usa timingSafeEqual. Observação, não gap da invariante. dubinc/dub#4415.

Axios, gin, Guzzle, Medusa, Documenso: invariante manteve. 3/3, 4/4, 11/11. Não abri issue. Abrir card pra dizer "olhei e está ok" é o mesmo ruído que o modelo solto fabrica.

A prova compacta, oito alvos, estático, sem PoC:

Alvo O que a disciplina fez
Formbricks XSS não confirmado. Hardening CORS → #9081
axios, gin, Guzzle invariante manteve. Silêncio.
Dub 7/7 autenticam. Timing → #4415
Medusa, Documenso pagamento e arquivo: denominador cheio, 0 gap. Silêncio.
Plane 3 gaps de autorização. Canal privado. Sem path aqui.

Se o seu critério de sucesso for volume, metade dessa tabela parece desperdício. Pra mim as linhas em silêncio são as que sustentam as outras. Impacto verificável não é o mesmo que "issue aceita": a #9081 ganhou security e agent-ready; a #4415 entrou no Linear deles como ENG-1732. As duas continuam abertas.

4. As três portas

Disciplina de divulgação é a mesma disciplina de finding. Least privilege aqui é o blast radius do texto. HITL é você escolhendo a porta, não o modelo. Audit trail é a issue ou o e-mail, nunca o transcript.

Issue pública: hardening. Bypass, injeção, secret, autorização quebrada: privado primeiro. Sem SECURITY.md, é e-mail do mantenedor, não thread.

Copia. Cola no fim da sessão, antes de abrir o browser.

O e-mail da trilha, com os links do que foi aberto e o porquê do silêncio:

5. O que você monta amanhã

Pega o repo que você mantém. Escreve uma invariante falsificável antes de abrir arquivo. "Todo webhook de entrada verifica assinatura antes de mutar estado" serve.

Roda o scanner primeiro. Depois pede ao modelo o passe só naquela invariante, com denominador e arquivo:linha, sem PoC, sem fix na mesma sessão.

Se voltar "23 findings" sem X de Y e sem citação, você não tem auditoria. Tem lista. Faltou direção. Faltou disciplina.

No último output de "audit" que um modelo te deu: você teria aberto issue pública, mandado e-mail privado, ou deixado nada? Se a resposta honesta for "ia colar o relatório inteiro no tracker", a porta ainda está solta.


Sobre o produto: o prompt é privado. Neste post vai o esqueleto. Por trás dele tem regras numa knowledge base e skills no harness. O pack (metodologia, cases, templates) está sendo refinado. Se você quer ser avisado quando sair mais, ou tem caso de uso específico, me chama no LinkedIn ou GitHub.

Top comments (0)