Найдите состояние гонки в эндпоинте перевода денег
Этот обработчик платежа списывает с одного баланса и зачисляет на другой. Найдите изъян бизнес-логики, который атакующий эксплуатирует множеством параллельных запросов.
Ограничения:
- баланс читается, проверяется и пишется отдельными шагами
- операция не обёрнута блокировкой или транзакцией
- атакующий шлёт тысячи
POST /payпараллельно
app.post("/pay", async (req, res) => {
const acc = await db.getAccount(req.user.id);
if (acc.balance >= req.body.amount) {
acc.balance -= req.body.amount; // read-modify-write, без блокировки
await db.save(acc);
}
});
Определите причину.
Гонка TOCTOU: чтение-проверка-запись не атомарны, поэтому тысячи параллельных запросов читают один баланс, все проходят проверку >= и каждый списывает — атакующий тратит куда больше, чем есть. Фикс: сделать атомарным — транзакция с построчной (пессимистичной) блокировкой или один условный апдейт (UPDATE ... SET balance = balance - :amt WHERE balance >= :amt).
- ✗Принимать гонку за проблему CSRF или rate limiting
- ✗Считать, что проверка >= перед записью защищает при конкурентности
- ✗Лечить симптом лимитом запросов вместо атомарности операции
- →Почему условный UPDATE одним запросом устраняет гонку у корня?
- →Чем пессимистичная блокировка строки отличается от лимита запросов здесь?
Уязвимость
const acc = await db.getAccount(req.user.id);
if (acc.balance >= req.body.amount) {
acc.balance -= req.body.amount; // read-modify-write, без блокировки
await db.save(acc);
}
Чтение баланса, проверка >= и запись разнесены во времени и не атомарны. При тысячах параллельных POST /pay все запросы читают один и тот же баланс, все проходят проверку и каждый списывает — это TOCTOU race condition, атакующий тратит больше, чем есть.
Исправление
Сделать проверку и запись неделимыми. Вариант с условным апдейтом:
UPDATE accounts
SET balance = balance - :amount
WHERE id = :id AND balance >= :amount; -- 0 строк = недостаточно средств
Или транзакция с пессимистичной блокировкой строки (SELECT ... FOR UPDATE).
✅ Условный UPDATE/блокировка делают проверку и списание одним атомарным шагом — гонка исчезает. ⚠️ Rate limiting и CSRF-токены полезны, но саму гонку не закрывают.