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.
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:
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.
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.
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
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".
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 |
|---|---|
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.
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:
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.