Início › Blog › Segurança de webhook do Stripe
Segurança de webhook do Stripe: 7 erros que liberam acesso de graça
12 de outubro de 2026 · ~8 min de leitura
O webhook do Stripe é onde o seu SaaS decide "essa pessoa pagou, pode liberar". É também um endpoint público na internet que qualquer um pode chamar. Se ele confia no que recebe sem verificar, alguém manda um checkout.session.completed falso e vira cliente pagante sem pagar nada. Estes são os 7 erros que mais vemos — com o código errado e o certo.
/api/stripe-webhook está no ar. Um atacante dá um POST com um JSON imitando um pagamento concluído. Se você não verificar a assinatura, o seu código libera o acesso. Grátis, ilimitado, e sem aparecer no painel do Stripe.
1. Não verificar a assinatura
O erro mais grave: ler o corpo da requisição e confiar nele. Todo evento legítimo do Stripe vem com o cabeçalho Stripe-Signature, um HMAC do corpo com o seu segredo do webhook (whsec_…). Sem checar isso, o endpoint aceita qualquer POST.
// ❌ confia no corpo cru const event = JSON.parse(body); liberarAcesso(event.data.object); // ✅ verifica a assinatura (SDK do Stripe) let event; try { event = stripe.webhooks.constructEvent(body, sig, endpointSecret); } catch (e) { return res.status(400).send('assinatura inválida'); }
2. Verificar com o corpo já parseado (quebra a assinatura)
A assinatura é calculada sobre os bytes exatos do corpo. Se um middleware (ex.: express.json()) parsear o JSON antes, você perde o corpo bruto e a verificação sempre falha — aí vem a tentação de "desligar a checagem pra funcionar". O certo é entregar o corpo bruto só nessa rota.
// ✅ corpo bruto apenas no webhook app.post('/api/stripe-webhook', express.raw({ type: 'application/json' }), handler);
Em Next.js (App Router) use await req.text(); em Pages/Workers, await request.text() — nunca .json() antes de verificar.
3. Liberar no evento errado (o caso Pix/Boleto)
O checkout.session.completed dispara quando o checkout é finalizado — mas em pagamentos assíncronos (Pix, boleto, débito em conta) isso acontece antes de o dinheiro cair. Liberar aí entrega o produto sem pagamento confirmado. Cheque o payment_status e trate o evento assíncrono.
switch (event.type) {
case 'checkout.session.completed': {
const s = event.data.object;
if (s.payment_status === 'paid') liberar(s); // cartão: já pago
break; // Pix/boleto: payment_status = 'unpaid' aqui, NÃO libera
}
case 'checkout.session.async_payment_succeeded':
liberar(event.data.object); break; // dinheiro caiu
}
4. Nenhuma idempotência (processar duas vezes)
O Stripe reenvia o webhook se não receber 200 rápido — e às vezes entrega o mesmo evento mais de uma vez. Sem deduplicação, você credita o dobro, libera duas vezes ou manda dois e-mails. Guarde o event.id e ignore repetidos.
if (await jaProcessado(event.id)) return res.json({ ok: true });
await marcarProcessado(event.id);
await liberar(event.data.object);
5. Fazer o trabalho pesado antes de responder
Se você gera PDF, chama IA ou roda uma fila dentro do handler, o Stripe pode estourar o timeout (~alguns segundos), achar que falhou e reenviar — multiplicando o efeito. Confirme rápido (200) e processe de forma assíncrona.
// ✅ enfileira e responde na hora
await fila.enqueue({ sessionId: s.id });
return res.json({ received: true });
6. Confiar no valor/preço que veio no evento
Trate o evento como "gatilho", não como fonte de verdade do valor. Recupere a sessão/pagamento pela API do Stripe (ou confira contra o seu pedido) e valide valor e moeda antes de liberar — evita manipulação e libera o plano certo.
const full = await stripe.checkout.sessions.retrieve(s.id);
if (full.amount_total !== pedido.valorEsperado) return;
7. Segredo errado, exposto ou sem HTTPS
- O
whsec_é por endpoint e por ambiente (teste ≠ produção) — usar o errado faz tudo falhar; é segredo de servidor, nunca vai pro front nem pro Git. - O endpoint tem que ser HTTPS. A tolerância de tempo padrão do SDK já barra replays antigos — não aumente sem motivo.
- Assine só os eventos que você usa, e deixe o endpoint fail-closed: assinatura inválida → 400, e nada é liberado.
stripe listen e stripe trigger checkout.session.completed da Stripe CLI para disparar eventos reais em dev; depois dê um curl com um corpo qualquer e sem assinatura no seu endpoint — ele tem que responder 400. Se responder 200 e liberar algo, a verificação está faltando. Protegendo pagamento, veja também o guia de RLS do Supabase e os erros de Firebase Rules.
Seu fluxo de pagamento está mesmo fechado?
O SAAS SHIELD revisa seu repositório e checa o webhook do Stripe, a liberação "pagou→libera" e a autorização — cada falha com local, evidência e correção.
Solicitar auditoria →