const TransferSchema = z.object({ toUserId: z.string(), amount: z.number() });
const { toUserId, amount } = TransferSchema.parse(req.body);
if (sender.balance < amount) throw new Error("Insufficient funds");
await transfer(sender.id, toUserId, amount);
It passes the balance check and moves 500 from the receiver to the sender
Any balance is greater than -500, so the check passes. Subtracting -500 adds 500 to the sender, and adding -500 takes 500 from the receiver. The attacker steals money with a transfer. Fix: z.number().int().positive() with a maximum, money stored in whole cents, and a database CHECK (balance >= 0) as a backstop.
"Zod rejects it" is the tempting wrong answer. z.number() accepts any number, including negatives and decimals. You must ask for .positive() and .int() yourself.
Read more