Segurança do webhook do Stripe
Esta skill executa a parte Stripe do scanner vibe-preflight (scripts/preflight.mjs ao lado deste arquivo,
Node 20+, sem dependências, sem rede). Ela encontra manipuladores de webhook em roteadores Next.js, pages/api,
Express e Supabase Edge Functions e verifica duas coisas no código. O resto desta página é uma revisão que você faz
lendo o manipulador, porque uma varredura de padrões não pode provar isso.
1. Execute a varredura
A partir da raiz do projeto:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only stripe --fail-on warning --format json
Claude Code preenche ${CLAUDE_SKILL_DIR}. Outros agentes: use a pasta desta skill, por exemplo
.agents/skills/stripe-webhook-security ou .claude/skills/stripe-webhook-security.
| Regra | Nível | O que significa |
|---|---|---|
stripe-webhook-unverified | erro | O manipulador atua em eventos do Stripe e nunca chama constructEvent / constructEventAsync. Qualquer um que encontrar a URL pode postar um checkout.session.completed falso. |
stripe-webhook-parsed-body | aviso | A assinatura é verificada, mas em relação a um corpo que já foi analisado. O Stripe assina exatamente os bytes que enviou, então eventos reais falham com 400 e alguém acaba removendo a verificação. |
--only stripe encontra um manipulador por nomes de eventos do Stripe, pela leitura do cabeçalho stripe-signature ou por um
caminho de webhook em um arquivo que menciona Stripe. Se relatar 0 erros mas o usuário sabe que tem um webhook, encontre
o manipulador você mesmo e faça a revisão abaixo de qualquer maneira.
2. Corrija a verificação
Manipulador de rota (Next.js App Router, Remix, Hono e outros manipuladores estilo fetch):
export async function POST(request: Request) {
const body = await request.text(); // corpo bruto, nunca request.json()
const signature = request.headers.get("stripe-signature");
if (!signature) return new Response("Assinatura ausente", { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Assinatura inválida", { status: 400 });
}
// manipular evento
return Response.json({ received: true });
}
Express: registre a rota do webhook com express.raw({ type: "application/json" }) antes
de app.use(express.json()), e passe req.body (que será um Buffer) para constructEvent.
Next.js pages/api: export const config = { api: { bodyParser: false } }; e leia a requisição bruta em um
Buffer antes de verificar.
Supabase Edge Functions e outros runtimes Deno / edge:
await stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider()).
3. Revise o restante lendo o manipulador
O scanner não verifica estes itens. Olhe o código e informe ao usuário o que você encontrou para cada um.
- Segredo.
STRIPE_WEBHOOK_SECRET(whsec_...) vem das variáveis de ambiente do servidor, nunca de uma variávelNEXT_PUBLIC_/VITE_ou do código-fonte. O modo de teste e o modo ao vivo têm segredos diferentes, assim como cada endpoint e o CLIstripe listen. - Repetição.
constructEventrejeita uma assinatura mais antiga que sua tolerância (300 segundos por padrão). Deixe o argumento de tolerância de fora ou mantenha-o pequeno. Nunca capture o erro e prossiga. - Idempotência. O Stripe pode entregar o mesmo evento mais de uma vez. Torne a escrita naturalmente
repetível (um upsert usando o ID do Stripe como chave, definindo um status em vez de adicionar créditos) ou armazene
event.idem uma tabela com uma restrição única e ignore os IDs já vistos. Examine com cuidado qualquer coisa que adicione: créditos, saldo, estoque. - Ordem. Os eventos podem chegar fora de ordem. Para assinaturas, busque o objeto atual da API
(
stripe.subscriptions.retrieve) ou compare timestamps em vez de confiar na ordem dos eventos. - Confiança. Extraia o usuário e o plano do evento verificado (
client_reference_id,metadata, o ID do cliente que você armazenou no checkout), nunca de uma string de consulta ou de uma segunda requisição não assinada. O redirecionamento da página de sucesso não prova nada: conceda o acesso a partir do webhook. - Resposta. Retorne 2xx rapidamente assim que o evento for armazenado ou tratado. Trabalho lento vai para uma fila. Retorne 400 apenas para uma assinatura inválida, para que as tentativas do Stripe continuem significativas.
- Lista de eventos. Inscreva o endpoint apenas nos eventos que o código trata, e ignore outros tipos sem erro.
4. Relatório
Liste os achados do scanner, depois uma linha por item da revisão: ok, corrigido ou uma pergunta para o usuário (por
exemplo "créditos são adicionados em cada entrega: um evento repetido deve adicioná-los novamente?"). Execute o scanner novamente
após a correção e mostre sua última linha. Para testar de verdade, sugira stripe listen --forward-to localhost:3000/api/webhooks e stripe trigger checkout.session.completed com o Stripe CLI.
Um falso positivo confirmado pode ser silenciado com // vibe-preflight-ignore <rule-id> na linha ou
na linha acima dela, com o motivo no comentário.