Загрузка...

Race Condition in telegram bots

Thread in Python created by Lebowsky13 Dec 29, 2025. (bumped Feb 7, 2026) 338 views

  1. Lebowsky13
    Всем пламенный новогодний :smile_victory:


    У кого боты шопы, вы Race Condition пофиксили у себя?
    Нет?
    Даже не слышали о таком?
    Тогда мы идем к вам.
    Тогда будьте готовы что вам вынесут весь товар.
    Я не шучу.



    Статья написана исключительно в образовательных целях. Хотя мне абсолютно нас рать, как вы это будете использовать.
    И рассчитана на любителей повайбкодить, или начинающих разработчиков. Но я думаю это будет полезно всем, кто учился по ютуб, там подобные вещи редко вспоминают почему-то.



    Вижу как массово плодятся профессиональные вайб кодеры, эта проблема наблюдается много где, просто мало кто знает как её правильно эксплуатировать.
    И вообще исследуя (легально) всемирную паутину, очень интересно наблюдать, как снизился уровень безопасности с появлением ИИ.

    Давайте сначала разберемся, что же это такое Race Condition?




    Вроде пока ничего не понятно, давайте ближе к практике.
    Ниже показан пример ПРОСТОГО бота, с этой уязвимостью.
    Вроде на первый взгляд, логика выглядит нормально, и да, вы правы, в однопоточных приложениях это возможно и будет вполне себе работать, но вот в многопоточных системах есть небольшой нюансик.

    Нас интересует только функция buy_item​
    Python
    import asyncio
    from aiogram import Bot, Dispatcher, types, F
    from aiogram.filters import Command


    user_balance = 100
    item_price = 100
    user_inventory = []

    bot = Bot(token="YOUR_TOKEN")
    dp = Dispatcher()

    @dp.message(Command("start"))
    async def start(message: types.Message):
    await message.answer(f"Ваш баланс: {user_balance} . Цена {item_price}.")



    @dp.message(Command("buy"))
    async def buy_item(message: types.Message):
    global user_balance
    # Вот тут то и происходит магия
    if user_balance >= item_price:
    # Тут имитация запроса к базе данных
    # именно в этом месте появляется окно для атаки
    # Представим что одновременно прилетает 3 запроса на проверку баланса
    # База данных отдаст баланс 100, так как его ещё не списало, списание
    # происходит ниже
    await asyncio.sleep(1)

    # соответственно все 3 запроса проходят проверку, и товар выдается
    # а в базу пытается записать отрицательный баланс

    # только тут списывается баланс, но это нам уже не особо важно
    # даже если у вас есть проверка
    user_balance -= item_price
    # Даже если на обновлении баланса выдает ошибку, мы же не хотим чтобы
    # бот упал правда? поэтому скорее всего вы это обернули в try Except
    # Программа продолжает выполнение, и выдает товар
    user_inventory.append("Товар")
    await message.answer(" Успешная покупка")
    else:
    await message.answer(" Недостаточно средств")

    @dp.message(Command("status"))
    async def status(message: types.Message):
    await message.answer(f"Баланс: {user_balance}\nИнвентарь: {len(user_inventory)} шт.")

    async def main():
    await dp.start_polling(bot)

    if __name__ == "__main__":
    asyncio.run(main())
    Вот как это сломать на практике:

    [IMG]

    Да, так просто, можно эксплуатировать данную уязвимость.
    Уже чувствую ваше желание написать костыль в виде дополнительных проверок, но зачем изобретать велосипед?
    Пофиксить можно весьма простыми и элегантными решениями, использовать менеджер контекста, либо делать проверки на атомарном уровне.

    НО ЛУЧШЕ ИСПОЛЬЗОВАТЬ АТОМАРНЫЙ КОНТЕКСТ:
    Давайте сначала разберемся что такое атомарность на простом примере:​
    Атомарность - это свойство операции быть выполненной либо полностью, либо не выполненной вовсе. Промежуточных состояний нет.

    Очень простой пример:

    Допустим, вы переводите 100 рублей с одного счёта на другой. Атомарная операция означает, что произойдёт только одно из двух:

    1. Полностью: 100 рублей спишутся с первого счёта И зачислятся на второй.
    2. Не выполнится вовсе: если что-то пошло не так (например, пропал свет), то деньги не спишутся с первого счёта и не зачислятся на второй.

    Неатомарная (разорванная) операция могла бы привести к некорректному состоянию: деньги уже списались с первого счёта, но на второй не дошли, то есть "исчезли". Атомарность это исключает.
    Python
    async with aiosqlite.connect("shop.db") as db:
    await db.execute("UPDATE users SET balance = balance - ? WHERE id = ? AND balance >= ?",
    (price, user_id, price)
    # Атомарность гарантируется самим SQL-запросом и транзакцией aiosqlite
    # В одном запросе проверяется и условие, и выполнение списания
    # Невозмoжно состояние, когда проверка и списание разделены

    # Благодаря условию balance >= ?:

    # Два параллельных списания не приведут к отрицательному балансу
    # SQLite блокирует строку при UPDATE, обеспечивая последовательность

    НО МОЯ ЗАДАЧА, ПОКАЗАТЬ ВАМ, КАК ВОЗНИКАЕТ RACE CONDITION, и ПРИМИТИВНЫЙ СПОСОБ пофиксить его, а не учить вас кодить.

    Менеджер контекста гарантирует что единственный поток будет выполнятся, все остальные будут "ждать".

    вот как просто, я изменил функцию покупки


    Python
    from async import Lock

    # Создаем замок
    purchase_lock = Lock()


    @dp.message(Command("buy"))
    async def buy_item(message: types.Message):

    global user_balance

    async with purchase_lock:
    # Тут создается контекстный менеджер, который гарантирует что
    # все остальные потоки будут ждать, пока замок закрыт Lock
    if user_balance >= item_price:

    await asyncio.sleep(1)
    user_balance -= item_price
    user_inventory.append("Товар")
    await message.answer(f" Успешно! Баланс: {user_balance}")

    else:

    await message.answer(" Недостаточно средств.")

    Как видите ничего сложного, это работает точно так-же на inline кнопках, и не только в телеграм ботах.
    И вообще Race Condition бывает очень хитрый и непредсказуемый. Он может возникать как на уровне базы данных, так и на остальных слоях, я показал очень примитивный пример. Context manager конечно хорошо, но лучше всего проверять это всё атомарно прямо в базе данных. Различные ORM так же представляют готовые решение.


    Пробуем теперь эксплуатировать уязвимость:

    [IMG]
    Вот и всё. Чтобы понять, почему это происходит, вам достаточно понять как работает Async и многопоточность. Здесь я это разбирать не стану, есть много материала в интернете и не одна книга об этом написана. Хотя многие я думаю и так поняли на интуитивном уровне. Я лично сначала это увидел, и лишь спустя некоторые время узнал, как это называется.


    Надеюсь эта информация сохранит ваши нервы и балансы.
    Молодцы что дочитали до конца.
    С Наступающим Новым Годом! :smile_victory: :smile_good:
     
  2. gcc_machine
    все, кто нормально кодит юзают транзакции, которые встроены почти во все популярные базы, а твой метод банальный и старый, многие посчитают это говнокодиком
     
    1. View previous comments (6)
    2. gcc_machine
      avatarLebowsky13, так зачем учить людей говнокоду? в чем проблема была написать все правильно, хотя бы добавить lock для каждого юзера, а не глобальный лок, который так же обходится двумя ботами, где фикс проблемы?
    3. мя_у
      avatargcc_machine , zabey on sam sozdal problemy, sam napisal reshenie, i sam sebe stavit like :duck_like:
    4. Lebowsky13 Topic starter
      avatargcc_machine , Дружище, я не учу никого кодить, цель темы объяснить природу race condition. Я отредактировал немного и сделал акцент на использование атомарных операций, без негатива.
      Примеры абстрактные, суть как эксплуатировать уязвимость ясна.
      Я понимаю, что я тебе когда-то резко ответил, прости, но с твоей стороны это выглядело как манипуляции + ты за слова почему-то не отвечаешь) Сказал что поможешь, а пропизделся, ну да ладно куй с этим, но у тебя хватило наглости ещё писать и что-то там просить чуть ли не требовать. Непонятный мув вообще с твоей стороны был) Поэтому я резко тебе и ответил
      Без обид)
    5. View the next comments (1)
  3. GreatestDreamer
    Не актуально с базами данных.
    Базы обязаны соответствовать ACID, такая тема не прокатит

    UPD:
    (A)CID - Atomicity - атомарность AKA транзакции
     
    1. View previous comments (4)
    2. GreatestDreamer
      avatarLebowsky13, ты действительно упомянул данный момент, но сама статья вводит в заблуждение, что нужно использовать Lock(). По моему мнению, тебе следовало показать пример с проверкой баланса и списания в рамках использования атомарного контекста, а не с механизмом блокировок.
    3. Lebowsky13 Topic starter
      avatarGreatestDreamer, я немного отредактировал, чтобы подчеркнуть что нужно использовать атомарные операции, а так же добавил простой пример, но все же придерживаюсь объяснять такие вещи на более простых, понятных и абстрактных примерах
    4. GreatestDreamer
  4. TheBoossya
    TheBoossya Dec 30, 2025 134 Aug 28, 2019
    чел переизобрел мьютекс
     
  5. AS7RID
    AS7RID Jan 4, 2026 Первоклассный пушистик 17,648 Jun 11, 2019
    Хз почему сверху говно месят. На говнокод похуй, пример есть пример, чем понятнее он для авг питониста - тем лучше, прошаренные челы и так уже знают как делать лучше
    Единственный доеб только к этому, ибо не пояснил че, как и почему, а имхо надо было. Многие пишут хуевые запросы к бд и из-за этого тоже обсираются с race condition
    Mutex база. Имплементация сверх хуевая, но понятная. В ботах такое юзать не надо, а вот в другом асинхронном полу-говнокоде можно
     
    1. Lebowsky13 Topic starter
      Я вот тоже не совсем понял, против критики не имею ничего против, но считаю, было бы лучше как-то критиковать в примерах. Я с радостью прикрепил бы это к статьи и указал авторство.
      Это я уже накрутил по просьбе "трудящихся", хотя лично считаю этот пример не очень, мне, будучи новичку не совсем было бы понятно, вот этот код с aiosqlite , ибо лично я начинал с другой БД.
      Так же и с атомарностью, вместо того, чтобы разбирать на примере баз данных (коих много), мне было проще это всё понять в такой говно-примитивной форме "на пальцах".
      Да и RC не только в базах данных существуют, с таким же успехом, можно забагать много чего, даже не обязательно это должно быть многопоточное приложение.
      Но опять таки, задача была показать примитивную природу RC, иначе мне нужно было бы писать простыню текста, упоминать и разбирать темы асинхронности, GIL, замыканий и т.д

      Да, к сожалению очень много, кто бы там что не говорил, что это не актуально, это очень даже актуально, меня именно это и вдохновило на написание этой статьи.


      Спасибо за комментарий, приму во внимание и немного отредачу
Loading...