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.

O ataque, em uma linha: seu endpoint /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

Como testar hoje: use o 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 →