{"slug": "o-prompt-de-appsec-que-eu-criei-achou-4-gaps-de-seguranca", "title": "O prompt de AppSec que eu criei achou 4 gaps de segurança", "summary": "A developer created an AppSec prompt that found four security gaps in open-source projects, including an authorization flaw affecting one of 174 routes. The prompt enforces a contract that limits what the LLM can assert, distinguishing real findings from false positives. The developer emphasizes that static scanners like Semgrep cannot detect missing authorization controls, which require semantic understanding.", "body_md": "Pessoal, eu quase mandei uma lista de 23 \"vulnerabilidades\" para projetos de software livre.\n\nEsses dias rodei um prompt de AppSec próprio em alguns repositórios conhecidos.\n\nXSS, 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.\n\nAí eu parei e pensei: se eu fosse o cara do outro lado da issue, eu levaria isso a sério?\n\nA 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.\n\nO 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.\n\nEsse é o ponto que eu demorei pra aceitar.\n\nO 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?\n\nDireção amarra três coisas **antes** da leitura:\n\nDisciplina é o que corta o teatro. O texto que o modelo vê antes de abrir arquivo:\n\n```\nPadrão: OWASP ASVS 5.0, com o ID do requisito. \"Boas práticas\" não conta.\nInvariante: uma frase, falsificável.\nRelatório: finding, observação e hipótese não misturam.\n\nSem arquivo:linha → não é finding.\nCWE inventado → encerra a sessão.\nNeeds-runtime → não vira card.\nSem X de Y → não é hunt.\nEstático. Sem PoC. Sem request no ambiente deles.\nA sessão que encontra não é a sessão que corrige.\n```\n\nEsse último item parece burocracia até o modelo achar um gap e, empolgado, reescrever o `where`\n\ndo Prisma no mesmo fôlego, sem teste. Finding de um lado. Fix de outro.\n\nO 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.\n\nUm limite honesto: isso só rende em modelo de raciocínio alto. Num modelo fraco, prompt bom organiza melhor o erro.\n\nScanner 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.\n\nO 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.\n\nPor 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:\n\n```\nsemgrep --config p/default --config p/typescript --config p/react \\\n        --config p/nextjs --config p/owasp-top-ten \\\n        --config p/javascript --config p/secrets\n```\n\n**23 findings. 17 eram unsafe-formatstring em template. Zero de autorização.**\n\nAutorizaçã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`\n\n.\n\nO hunt começa invertido. Você escreve a invariante antes de abrir o arquivo. Enumera caminhos. Classifica: `enforced-explicitly`\n\n, `inherited`\n\n, `gap`\n\n, `out-of-scope`\n\n. No fim tem denominador. \"Achei um IDOR\" sem \"1 de 174\" não é hunt. É vibe.\n\nNaquele 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.\n\n| Scanner | Hunt dirigido | |\n|---|---|---|\n| Enxerga | padrão presente | controle ausente |\n| Bom em | injeção, XSS, secret, header, crypto fraca | o 1 caminho em 100 que pula o guard |\n| Custo | centavos, a cada PR | dólares, periódico |\n| Falso positivo | ruído sintático | alucinação de finding, se você soltar o denominador |\n| Ponto cego | lógica de negócio | cobertura exaustiva do trivial |\n\nDuas cadências, duas perguntas. Verde no CI nunca respondeu \"a invariante ainda vale\".\n\nA falha mais comum de IA em segurança é fabricar achado para não devolver silêncio. Disciplina se mede nisso.\n\nBusca ingênua por `dangerouslySetInnerHTML`\n\nem 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 (`*`\n\n+ credentials), que o navegador rejeita. [formbricks#9081](https://github.com/formbricks/formbricks/issues/9081). Tinha um SVG `Needs-runtime`\n\n. Não virou card. Prompt solto teria aberto dois. O dirigido abriu um.\n\nWebhook de entrada: 7 de 7 autenticam o remetente. Semgrep no escopo: 0. Autenticidade não é padrão sintático. Dois HMAC com `!==`\n\nno mesmo repo que já usa `timingSafeEqual`\n\n. Observação, não gap da invariante. [dubinc/dub#4415](https://github.com/dubinc/dub/issues/4415).\n\nAxios, 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.\n\nA prova compacta, oito alvos, estático, sem PoC:\n\n| Alvo | O que a disciplina fez |\n|---|---|\n|\n\nSe 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`\n\ne `agent-ready`\n\n; a #4415 entrou no Linear deles como ENG-1732. As duas continuam abertas.\n\nDisciplina 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.\n\nIssue pública: hardening. Bypass, injeção, secret, autorização quebrada: privado primeiro. Sem `SECURITY.md`\n\n, é e-mail do mantenedor, não thread.\n\nCopia. Cola no fim da sessão, antes de abrir o browser.\n\nO e-mail da trilha, com os links do que foi aberto e o porquê do silêncio:\n\nPega 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.\n\nRoda o scanner primeiro. Depois pede ao modelo o passe só naquela invariante, com denominador e `arquivo:linha`\n\n, sem PoC, sem fix na mesma sessão.\n\nSe voltar \"23 findings\" sem `X`\n\nde `Y`\n\ne sem citação, você não tem auditoria. Tem lista. Faltou direção. Faltou disciplina.\n\nNo ú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.\n\n**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](https://linkedin.com/in/tiagovilasboas) ou [GitHub](https://github.com/tiagoboaventura).", "url": "https://wpnews.pro/news/o-prompt-de-appsec-que-eu-criei-achou-4-gaps-de-seguranca", "canonical_source": "https://dev.to/tiagovilasboas/prompt-appsec-4-gaps-autorizacao-1k77", "published_at": "2026-09-02 03:58:17+00:00", "updated_at": "2026-09-02 04:24:32.258950+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety", "developer-tools", "artificial-intelligence"], "entities": ["OWASP", "Semgrep", "CodeQL", "Bandit", "Formbricks", "Dub", "Prisma"], "alternates": {"html": "https://wpnews.pro/news/o-prompt-de-appsec-que-eu-criei-achou-4-gaps-de-seguranca", "markdown": "https://wpnews.pro/news/o-prompt-de-appsec-que-eu-criei-achou-4-gaps-de-seguranca.md", "text": "https://wpnews.pro/news/o-prompt-de-appsec-que-eu-criei-achou-4-gaps-de-seguranca.txt", "jsonld": "https://wpnews.pro/news/o-prompt-de-appsec-que-eu-criei-achou-4-gaps-de-seguranca.jsonld"}}