● CWE-362 · CWE-367

Condições de corrida e TOCTOU (CWE-362, CWE-367): como requisições paralelas furam verificações e como corrigir

Um código que lê um valor, verifica e depois grava deixa duas requisições que chegam no mesmo instante passarem pela verificação, e assim um vale-presente é resgatado duas vezes ou um saldo fica negativo. Como funciona a janela entre verificar e agir, por que verificações na aplicação não conseguem fechá-la e como um único UPDATE ... RETURNING condicional, bloqueios de linha e restrições do banco tornam a operação atômica, com um exemplo em Python e PostgreSQL.

Explicação em Linguagem Simples (ELI5)

Um cinema tem dois caixas e só um lugar livre na fileira F. Os dois olham o mapa de assentos no mesmo segundo, ambos veem a poltrona F7 livre e ambos a vendem. Cada caixa conferiu antes de vender; o problema é que a conferência e a venda foram dois momentos separados, e o outro caixa agiu no meio. A correção é um único bloco de ingressos do qual os dois precisam destacar: quem destacar o último F7 fica com ele, e o outro encontra o bloco vazio. Conferir e pegar viram uma só ação.

Conceitos Centrais e Termos

TOCTOU (do momento da verificação ao momento do uso)
O estado verificado pode mudar antes de ser usado. Em código web, a verificação costuma ser um SELECT e o uso é um UPDATE posterior, com outras requisições rodando no meio.
Janela de corrida e requisições paralelas
A janela muitas vezes é de poucos milissegundos, mas atacantes conseguem alinhar dezenas de requisições para chegarem juntas. A pesquisa da PortSwigger de 2023 mostrou que enviar os últimos bytes de muitas requisições HTTP/2 num único pacote faz com que cheguem com cerca de um milissegundo de diferença.
Atualização condicional atômica
UPDATE ... WHERE code = %s AND NOT redeemed RETURNING ... verifica e altera a linha numa única instrução. Só um chamador recebe uma linha de volta; os outros não recebem nenhuma.
Bloqueios de linha em Read Committed
No PostgreSQL, um segundo UPDATE na mesma linha espera a primeira transação e depois reavalia a cláusula WHERE contra a linha já confirmada. Essa reverificação é o que torna a atualização condicional segura sem elevar o nível de isolamento.
Restrições e chaves de idempotência
Índices UNIQUE e CHECK (balance_cents >= 0) fazem uma corrida perdida falhar de forma visível em vez de gastar em dobro. Uma Idempotency-Key guardada sob um índice único permite que clientes repitam pagamentos sem duplicá-los.

Fluxo de Ataque Passo a Paso

Passo 1

O atacante tem um vale-presente válido

Ele tem um único vale de valor fixo e uma conta para receber o crédito.

Passo 2

Ele envia muitos resgates ao mesmo tempo

Vinte requisições com o mesmo código chegam dentro de um milissegundo. Duas requisições paralelas usando o código de teste TEST-0001 ilustram o caso.

Passo 3

Todas as requisições passam pela verificação

Cada SELECT roda antes que qualquer requisição tenha marcado o vale como resgatado, então todas veem redeemed = false.

Passo 4

O saldo é creditado várias vezes

Cada requisição soma o valor do vale antes de a marca ser gravada. Em 2015 Egor Homakov mostrou que transferências paralelas entre cartões-presente da Starbucks podiam criar saldo do nada.

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
# gift_cards.py: verificação e uso são instruções separadas
from .db import pool


# toda requisição paralela pode ver redeemed = false antes que qualquer uma grave
def redeem_gift_card(user_id: int, code: str) -> int:
    with pool.connection() as conn:
        card = conn.execute("SELECT id, balance_cents, redeemed FROM gift_cards WHERE code = %s", (code,)).fetchone()
        if card is None or card[2]:
            raise ValueError("invalid or used card")
        conn.execute("UPDATE accounts SET balance_cents = balance_cents + %s WHERE user_id = %s", (card[1], user_id))
        conn.execute("UPDATE gift_cards SET redeemed = true WHERE id = %s", (card[0],))
        return card[1]
PATCH SEGURO E ROBUSTO
# gift_cards.py: verificação e uso acontecem numa só instrução
from .db import pool


# só um chamador recebe uma linha; os demais esperam o lock e veem NOT redeemed falhar
def redeem_gift_card(user_id: int, code: str) -> int:
    with pool.connection() as conn, conn.transaction():
        card = conn.execute(
            "UPDATE gift_cards SET redeemed = true, redeemed_by = %s, redeemed_at = now() "
            "WHERE code = %s AND NOT redeemed RETURNING balance_cents",
            (user_id, code),
        ).fetchone()
        if card is None:
            raise ValueError("invalid or used card")
        # crédito na mesma transação, então as duas mudanças são confirmadas juntas ou nenhuma
        conn.execute("UPDATE accounts SET balance_cents = balance_cents + %s WHERE user_id = %s", (card[0], user_id))
        return card[0]

Lista de Verificação de Segurança para Engenharia

Fontes

← Ver o diretório completo de segurança Todos os guias de vulnerabilidades →