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

작성자

카테고리:

← 피드로
DEV Community · Tiago Vilas Boas (Montanha) · 2026-09-02 개발(SW)

Pessoal, eu quase mandei uma lista de 23 “vulnerabilidades” pra mantenedores de código aberto.

XSS, CORS, CSP, um SVG suspeito. O modelo tinha devolvido volume. Eu sentia que tinha trabalhado. Aí parei. 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 authorization.

Broken authorization (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 authorization gaps 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.

Não é um contra o outro. O scanner pega injeção, XSS, secret, header. O hunt pega o caminho que usa o mesmo findUnique e esquece o dono. Um é CI. O outro é pergunta. Verde no pipeline nunca respondeu se 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.

Mesma classe, outro alvo. No Cal.com, 8 de 8 webhooks de entrada verificam o remetente antes de mutar. HitPay, o token do Google Calendar e o secret de credential-sync ainda comparam com !==. O BTCPay, no mesmo monorepo, já usa timingSafeEqual. cal.com#30100.

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.

Daemon local é o mesmo teste com outra forma. No Magnitude o ACN já escuta em loopback. Isso é controle. CORS na porta pública ainda refletia Origin: null. Markdown passava href sem allowlist de protocolo. Três issues no tracker que o README aponta: #78, #79, #80. Shell do agent (-c) não virou finding: é a ferramenta do produto. Dashboard de debug: o maintainer já tinha fechado essa classe no tracker. Não abri mais três cards no dia seguinte. Volume aqui também seria ruído.

A prova compacta, 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 Cal.com 8/8 autenticam. Timing → #30100 Medusa, Documenso payment idempotency e file namespace: denominador cheio, 0 gap. Silêncio. Magnitude daemon local. #78 #79 #80. Dashboard de debug: silêncio. Plane 3 gaps de object-level authorization. Canal privado. Sem path aqui.

Juice Shop e WebGoat não estão nesta tabela. São lab de propósito inseguro: o SQL concatenado já é o produto. Abrir issue ali seria o volume que este post recusa. O complemento deste fio: 13 findings no Juice Shop. Nenhuma virou issue. Lab calibra precisão. Não prova as três portas.

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. A #30100 é a mesma porta: hardening, sem PoC.

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, authorization gap: privado primeiro. Sem SECURITY.md, o README ainda pode apontar o tracker: aí hardening vai pra issue, como no Magnitude. O resto continua e-mail ou advisory, não thread de exploit.

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.

원문에서 계속 ↗