Последствия
- Бро, выручай, увели весь товар из магазина(( Думаю накрутили баланс, посмотри плиз. ©
Занятное сообщение? Особенно когда ты знаешь, что у твоего братана было больше, чем на 100k баксов товара. Но не будем думать о товаре и деньгах, а лучше поразмыслим над тем, как всё провернуть

Race Condition
Состояние гонки, или как принято говорить в более строгой академической среде - неопределённость параллелизма. Смысл этой атаки в том, что при параллельном выполнении кода можно «накрутить баланс». Ну, скажем, при положительном внутреннем балансе в 100$ снять трижды по 100, и накупить себе вещей на 300$. А так же применить скидочные купоны по несколько раз, промокоды, да и кучу всего поделать, что должно выполняться раз, а мы имеем возможность сделать несколько. Конечно, при условии, что программист не учёл этот фактор.
Как это происходит
Уязвимость появляется при параллельном одновременном доступе к общему ресурсу ( балансу в нашем случае). Здесь я приведу простой пример на Python.
Python:
balance = 170
local_allb = 0
def buy(by):
global balance,local_allb
# Получаем текущее значение баланса
priv_balance = balance
# Списываем сумму
priv_balance -= by
# Проверка баланса
if priv_balance <= 0:
print('Недостаточно средств!')
return False
# Счётчик суммы всех операций
local_allb += by
print(f'Средств списалось: {by}')
# Устанавливаем значение основного баланса
balance = priv_balance
print(f'Остаток: {balance}')
На балансе у нас 170 денег. Попробуем списать 50:
Python:
if buy(50) == False:
print(balance)
Получаем логичный ответ:
Пробуем списать 300 и видим сообщение о том, что денег нам не хватает:
Пока что всё работает хорошо.
А теперь, мой дорогой друг, я накидаю товаров ( в вымышленную корзину, естественно ) на сумму больше той, что имеется на балансе.
Python:
Basket = [10,20,30,40,50,60,70] # 280 денег
И напишу функцию, которая в параллельном выполняющемся многопотоке будет покупать все эти товары:
Python:
def attack():
with ThreadPoolExecutor(max_workers=len(basket)) as on1:
on1.map(buy,basket)
print(f'Значение баланса в итоге: {balance}')
Супер! Мы купили на 210 денег, при балансе в 170 и ещё осталось 60. Вот - простейший пример уязвимости данного типа. Но это в теории. На практике всё немного иначе. По сути, в примере выше, я смоделировал вариант для реализации Race Condition, но что усложняет эксплуатацию на удалённом ресурсе? Ответ один — временные задержки. И здесь речь идёт не только о времени задержки сети, но и внутренних задержках во время выполнения кода, по сути совокупности всего. И всё же я подчеркну — усложняет, но не лишает. Допустим я установлю функцией sleep задержку в 0.7 мс в функции buy(). И запущу 100 итераций атаки, результат уже будет иным:
Python:
for i in range(100):
local_allb = 0
balance = 170
attack()
print(f'Сумма всех выполненных операций: {local_allb}')
if local_allb > 170:
print('STOP!')
exit()
Лишь единожды из сотни раз мне удалось добиться общей суммы 280 и несколько раз удалось получить значение в 150 с остатком баланса 50 ( т.е. 30 денег взялись из неоткуда ). Это при установке одинаковой задержки в начале функции. Естественно, установка одинаковой временной задержки не гарантирует, что она будет строго 0.7 мс. Там будут свои погрешности, о чём я расскажу немного позже.
Теперь я попробую функцию sleep(0.7) переместить после получения данных о глобальном балансе в той же функции buy():
И так почти каждую итерацию из 100. Всё что мы сделали - это изменили позицию задержки. Уязвимость чувствительна к положению, в котором происходит задержка. Отсюда следует вывод: если потоки получили доступ к ресурсу, то вероятность того, что атака пройдёт успешно - больше.
Если мы создадим случайную задержку для каждого потока, к примеру - от 0.7 мс до 1.5 с, и установим её перед выполнением кода функции, мы заметно сократим вероятность успешной атаки, но не уберём её полностью. Такой результат я получил на 217 проходе, вероятность ниже, но уязвимость присутствует.
Раньше или позже мы получаем желаемое. Даже, если пинг между запросами серьёзно отличается ( отличие от 0.7 мс до 1.5 я считаю весомым ). Даже если какие либо задержки реализованы или возникают по определённым причинам в функции, всё равно, увеличивая количество проходов, мы увеличиваем вероятность того, что атака пройдёт успешно. Может на 217 проходе, а может и на 10217 проходе, но конечная цель может быть достигнута.
Решения
Из выше сказанного становиться понятно, что для реализации подобных функций нужно создавать некую очередь и не позволять потокам одновременно получать доступ к каким-либо данным. Как это сделать? Есть множества вариантов реализации, такие как: механизмы блокировок, семафоры, атомарный операции, асинхронный код. Главное - нужно определиться с тем, как быстро должен работать твой код, а так же какие проблемы могут возникнуть при реализации механизмов защиты. Исходя из ответов - определиться с реализацией.