No Vuln — empresa brasileira de pentest e auditoria de segurança ofensiva
Voltar ao blog

Race condition em PIX: o bug que paga 2x devolução

Race condition em devolução PIX é o bug mais subestimado de fintech brasileira em 2026. Análise técnica do HTTP/2 single-packet attack em endpoints financeiros.

Vulnerabilidade·23 de maio de 2026·11 min de leitura
Diego Melo — autor
Diego Melo

Pesquisador de segurança e fundador da No Vuln

Race condition em PIX: o bug que paga 2x devolução

Race condition em endpoints de PIX é o bug mais subestimado de fintech brasileira em 2026. Não é teórico. Está em produção em quase toda fintech que não passou por pentest focado em race conditions HTTP/2. Quando explorado, transfere dinheiro real do banco para a conta atacante.

Esse post é análise técnica do padrão, com fluxo de exploração anonimizado e checklist de correção. Se você opera fintech, IP autorizada, gateway de pagamento ou qualquer sistema que processa fluxo financeiro com endpoint de devolução, refund, saque ou estorno — é leitura obrigatória.

O bug em 1 frase

Endpoint que processa devolução (ou saque, ou estorno) sem lock atômico aceita múltiplas execuções paralelas do mesmo pedido, transferindo dinheiro mais de uma vez antes de marcar o pedido como “já processado”.

Por que HTTP/2 single-packet attack mudou o jogo

Race conditions sempre existiram. O que mudou em 2020+ foi a capacidade técnica de disparar dezenas de requisições no mesmo pacote TCP via HTTP/2.

Pré-HTTP/2: cliente envia request 1, aguarda ACK, envia request 2. A latência de rede entre requests era de ~50-200ms. Race condition exige janela de microsegundos pra explorar de verdade. Em prática, raras.

Pós-HTTP/2 + multiplexing + single-packet attack: cliente envia 30 requests no MESMO pacote TCP. Servidor processa quase simultaneamente. Janela entre requests: microsegundos. Race condition que era teórica vira explorável trivialmente.

Ferramentas como Turbo Intruder (extensão do Burp Pro) implementam isso de forma nativa. Não é técnica obscura — é prática padrão em pentest moderno.

O endpoint vulnerável típico

Endpoint clássico de devolução PIX:

POST /api/v1/pix/transactions/{tx_id}/refund
Authorization: Bearer <token>

Implementação ingênua (pseudo-código):

  1. Carrega a transação: transaction = Transaction.get(tx_id)
  2. Checa: if transaction.status == 'refunded': raise AlreadyRefundedError()
  3. ← janela de race aqui ←
  4. Transfere: bank_api.transfer_back(payer, amount)
  5. Atualiza status: transaction.status = 'refunded'; transaction.save()

Entre o check if status == 'refunded' e o transaction.save() existe janela onde outra request paralela passa pelo mesmo check.

Resultado: 30 requests simultâneos → 30 transferências bancárias enquanto pedido marcado como “não devolvido ainda”.

A exploração na prática

Usando Turbo Intruder (script Python em Burp Pro):

  • Configurar engine com requestsPerConnection=30 e engine=Engine.BURP2
  • Enfileirar 30 requests POST idênticos com gate='race1'
  • Abrir o gate: engine.openGate('race1') — libera todos simultaneamente
  • Observar responses

Resultado em fintech vulnerável:

  • 1 request: 200 OK (devolução processada)
  • 29 requests: 200 OK também (todos processaram)
  • Conta bancária do “cliente” recebeu 30x R$ 1.000 = R$ 30.000
  • Status da transação: refunded (1 vez no DB)

Atacante gasta R$ 1.000 em uma compra, devolve, recebe R$ 30.000 de volta. Não é teórico — é o padrão exato que vemos repetir.

Onde isso aparece em fintech brasileira

Não é só PIX. Em pentest, encontramos race condition em:

Endpoints financeiros

  • Devolução PIX (cenário acima)
  • Estorno de cartão
  • Saque de saldo
  • Antecipação de recebível
  • Resgate de investimento

Endpoints de “limite”

  • Cupom single-use (cupom de R$ 100 usado 30x = R$ 3.000 desconto)
  • Cashback (race em cancelamento = cashback dobrado)
  • Voucher de promoção(limite “100 vouchers” virou 130)

Endpoints de autenticação

  • MFA bypass (race em validação de código OTP)
  • Bypass de rate limit (30 requests antes do counter incrementar)
  • Account recovery race

Endpoints de estado

  • Aprovação de transferência(aprovar 2x antes de virar “aprovada”)
  • Confirmação de pedido (estoque negativo)
  • Mudança de plano em SaaS (downgrade após cobrança, double upgrade)

Como corrigir — soluções erradas vs corretas

❌ Solução errada 1: time.sleep()

Adicionar time.sleep(0.1) entre o check e o write não resolve. Só reduz a janela. Trivialmente contornado com mais requests ou ferramenta mais agressiva.

❌ Solução errada 2: Lock em memória do Python

Usar threading.Lock() ou similar não resolve em produção. Lock só funciona dentro do mesmo processo. Em produção com múltiplos workers/instâncias (gunicorn, uwsgi, kubernetes), cada processo tem seu próprio lock. Race entre processos continua.

✅ Solução correta 1: Lock atômico no banco (PostgreSQL SELECT FOR UPDATE)

Dentro de uma transação do banco, usar SELECT FOR UPDATE bloqueia a linha no PostgreSQL. Outras requests aguardam até a primeira commit ou rollback. Pseudo-fluxo:

  1. Abrir transação DB
  2. transaction = Transaction.select_for_update(tx_id) — adquire lock
  3. Checar status
  4. Transferir dinheiro
  5. Atualizar status e salvar
  6. Commit (lock liberado automaticamente)

Funciona em ambiente distribuído (todos os workers compartilham o mesmo DB). É o baseline mínimo para qualquer endpoint financeiro.

✅ Solução correta 2: Idempotency Key (padrão Stripe / Adyen / Mercado Pago)

Cliente envia header Idempotency-Key: <uuid-único-por-operação>. Servidor:

  1. Verifica se a chave já foi processada (consulta em tabela ou cache)
  2. Se já foi: devolve a resposta cacheada (não reprocessa)
  3. Se não foi: cria registro com lock, processa, salva resposta, marca como completa
  4. Em retries ou races: chave existe → resposta cacheada devolvida

Esse é o padrão usado por Stripe, Adyen, Mercado Pago e qualquer adquirente sério. Não é opcional em fintech séria — é mandatório.

✅ Solução correta 3: Lock distribuído (Redis)

Quando a lógica atravessa múltiplos serviços (microservices) e DB lock não cobre, use Redis lock distribuído (Redlock pattern ou similar). Bloqueia em recurso lógico, não no DB.

Os 3 testes que toda fintech deveria ter no CI

Teste 1: Refund não duplicado sob race

Cria 1 transação, dispara 30 requests paralelas via threading, espera assert success_count == 1. Se mais de 1 passar, race aberto.

Teste 2: Idempotency key enforced

Faz request sem header Idempotency-Key, espera assert response.status == 400. Garante que endpoint financeiro rejeita requests sem idempotency.

Teste 3: Mesma idempotency key = mesma resposta

Faz 2 requests com mesma key, compara responses, espera assert r1.json() == r2.json() e assert get_processed_count() == 1.

CI que falha se race for reintroduzido. Esse é o controle preventivo definitivo.

O custo de não corrigir

Casos reais que vimos (anonimizados):

Endpoint vulnerávelImpacto máximo se explorado
Devolução PIXTransferência bancária 30x do valor original
Cupom single-use 50% offCupom usado N vezes em pedidos paralelos
Saque de saldoSaque maior que saldo disponível (saldo negativo)
MFA OTPBypass de 2FA (race entre check e expiração)
Estorno cartãoCrédito de N estornos via 1 transação

Em fintech regulada pelo Bacen, esse bug:

  • Viola Manual de Segurança PIX (controles operacionais)
  • Cria incidente reportável (BCB Resolução 1/2020)
  • Tem impacto financeiro direto e imediato (não potencial)
  • Pode disparar auditoria não programada se reincidente

O que fazer hoje

Se você opera fintech, gateway, IP, ou qualquer sistema com endpoints financeiros:

  1. Listar todos endpoints que mudam saldo, status ou contador
  2. Para cada um, validar: existe lock atômico? Idempotency key?
  3. Testar manualmente com Turbo Intruder (30 requests paralelas)
  4. Adicionar testes de race no CI (templates acima)
  5. Se não tem certeza: contratar pentest focado em race conditions

A maior parte das fintechs brasileiras que auditamos em 2026 tem pelo menos 1 endpoint vulnerável. Não porque o time é ruim — porque race condition em HTTP/2 mudou completamente nos últimos 4 anos e muita doc/blog antigos ainda recomendam padrões insuficientes.

Para conversa sobre pentest focado em PIX e fluxos financeiros, veja as páginas dedicadas: /seguranca-pix, /pentest-fintech e /pentest-meios-pagamento. Para entender outras falhas que dominam APIs em 2026, veja BOLA, BOPLA e BFLA.

Compartilhar

WhatsAppLinkedInX

Próximo passo

Quer aplicar isso ao seu sistema?

A No Vuln faz pentest profundo com a mesma metodologia descrita neste artigo. Solicite uma proposta — NDA bilateral em 24h, escopo definido em call técnica.