Você já viu uma vaga livre de longe, deu a volta com calma e quando chegou lá, outro carro já tinha estacionado? Pois é. Seu código pode estar fazendo isso, e o resultado pode ser um saque duplicado, um cupom usado centenas de vezes ou um invasor escrevendo onde não deveria.
O que raios é TOCTOU?
TOCTOU significa Time Of Check to Time Of Use (do momento da verificação ao momento do uso). É um tipo de race condition (condição de corrida) que acontece quando existe um intervalo de tempo entre o “Check” e o “Use”.
- Check: é o momento em que o programa verifica se algo é verdade, por exemplo: “essa vaga está livre?”, “tenho permissão para escrever neste arquivo?”, “tem saldo suficiente?”
- Use: é o momento em que o programa efetivamente age com base na verificação, por exemplo: “estaciono”, “escrevo o arquivo”, “realizo o saque”
Voltando à nossa analogia:
Você vê a vaga livre (check), mas entre ver e efetivamente estacionar (use), existe um tempo, e nesse tempo, outra pessoa pode ocupar essa vaga. Se você não checar novamente antes de encostar, você vai bater no carro que chegou antes de você.
Em uma aplicação, o problema é exatamente esse: o programa confia numa informação que já pode estar desatualizada no momento em que decide agir.
Mas por que isso acontece?
Uma das causas fundamentais do TOCTOU é a falta de atomicidade, ou seja, é quando uma operação que deveria ser indivisível é dividida em um check e um use separados, permitindo que outro agente altere o estado entre eles.
Quando você escreve:
verificar(x)
usar(x)
Essas duas etapas separadas fazem você assumir que nada mais vai mexer em x entre uma linha e outra. Mas isso só é segura se só existir um “carro” tentando estacionar por vez. Quando o assunto é programação, isso quase nunca é verdade, pois existem múltiplos processos e múltiplos usuários acessando o seu sistema ao mesmo tempo.
Exemplinho prático
import { access, open, constants } from "fs/promises";
async function escreverLog(caminho: string, dados: string) {
const podeEscrever = await access(caminho, constants.W_OK)
.then(() => true)
.catch(() => false);
if (podeEscrever) {
//janela de tempo entre check e use
const fd = await open(caminho, "w");
await fd.writeFile(dados);
await fd.close();
}
}
O programa pergunta: “Posso escrever neste arquivo?” (check via access). O sistema confirma que sim. Só que, entre essa confirmação e o open() de fato, um atacante pode trocar /tmp/log.txt por um symlink apontando para outro arquivo do sistema.
O programa, achando que ainda está escrevendo no arquivo original, escreve em um arquivo crítico do sistema usando a permissão que ele já tinha, sem nunca precisar burlar nada diretamente.
Uma observação importante
TOCTOU não é simplesmente “existe um intervalo entre check e use”. Esse pequeno detalhe é importante porque praticamente todo programa tem operações sequenciais. Ou seja, o problema não é existir tempo entre duas operações, é a possibilidade do recurso mudar nesse intervalo.
Então, TOCTOU é a forma específica de race condition, caracterizada pela separação entre verificar uma condição e utilizá-la.
Alguns exemplos de cenários reais de impacto
Cupons de desconto reutilizados
A aplicação verifica “esse cupom já foi usado?” e antes de marcar como usado, várias requisições simultâneas passam pela mesma checagem e todas veem o cupom como válido.
Estoque vendido além do disponível
Em momentos com alta demanda, múltiplas requisições verificam estoque disponível ao mesmo tempo, todas rebem “sim, tem estoque”, e o sistema vende mais unidades do que realmente existem.
Uma pergunta que ajuda a pegar esses casos: “O que acontece se, exatamente entre esta linha e a próxima, outro processo mexer nesse recurso?”
Como prevenir
1. Transforme check + use em uma única operação atômica
import { open } from "fs/promises";
async function criarArquivoComSeguranca(caminho: string, dados: string) {
try {
const fd = await open(caminho, "wx");
await fd.writeFile(dados);
await fd.close();
} catch (erro: any) {
if (erro.code === "EEXIST") {
// já existia. Trate aqui, sem abrir brecha
return;
}
throw erro;
}
}
O trecho open(caminho, "wx") não significa “qualquer escrita segura”. Ele é apropriado para o caso específico de criar o arquivo somente se ele não existir. Isso elimina a corrida “verificar se existe → criar”
2. Use um lock para proteger a região crítica
Um lock (trava) é uma forma de dizer: “enquanto eu estiver usando este recurso, ninguém mais entra até eu terminar”.
import { Mutex } from "async-mutex";
const lock = new Mutex();
async function sacar(conta: { saldo: number }, valor: number): Promise<boolean> {
// acquire() espera a vez, caso alguém já esteja "dentro" da região travada
const release = await lock.acquire();
try {
// a partir daqui, ninguém mais consegue executar esse trecho ao mesmo tempo.
if (conta.saldo >= valor) {
conta.saldo -= valor;
return true;
}
return false;
} finally {
release(); // libera a trava pro próximo que está esperando
}
}
3. Resolva check e use na mesma instrução no banco de dados
Exemplo:
const resultado = await pool.query(
"UPDATE contas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1",
[100, contaId]
);
if (resultado.rowCount === 0) {
throw new Error("Saldo insuficiente");
}
Se a condição não for mais verdadeira no momento exato da atualização, a linha simplesmente não é afetada (rowCount === 0). Sem necessidade de SELECTprévio. Sem janela de risco.
Resuminho maroto
| Pergunta | Resposta |
|---|---|
| O que é? | É a vulnerabilidade/condição de corrida usada pelo fato de a condição verificada poder mudar antes do uso. |
| Por que acontece? | O problema não é simplesmente uma operação não ser atômica. O ponto é que a decisão depende de uma condição que foi observada em um momento diferente daquele em que o recurso é utilizado. |
| Impacto típico | Cupons reutilizados, estoque vendido além do disponível, saques duplicados, escrita em arquivos indevidos |
| Regra de ouro | Sempre que possível, transforme "verificar + usar" em uma única operação atômica |
Nenhuma das técnicas acima é exclusiva de um cenário específico. A flag wxresolve para arquivos, o lock resolve para regiões críticas em memória, e a query atômica resolve para banco de dados, mas a ideia por trás das três é sempre a mesma: eliminar a janela entre check e use.
Da próxima vez que você escrever um if seguido de uma operação importante, vale a pena parar e se perguntar: e se, entre essas duas linhas, ou outra parte do sistema mexer nesse mesmo recurso? Se a resposta te preocupar, é um bom sinal de que ali existe uma vaga de estacionamento esperando para ser disputada hehehe.

Top comments (1)
Your explanation of TOCTOU as a specific form of race condition is spot on. The practical examples you provided, like discount coupons and inventory management, highlight just how critical it is to address these vulnerabilities in real-world applications. One improvement could be implementing locking mechanisms or atomic transactions to mitigate these issues, which can significantly enhance data integrity. If you're considering enhancements in this area, I'd be interested in discussing how I could contribute to the project's security features. What strategies are you currently exploring to handle these race conditions in your code?