avatar
Артём Шумейко
@artemshumeiko
24.11.2025 18:50
Как защитить приложение от DoS атак

Сегодня речь пойдет о том, как мы боролись с юзерами, кто тильтует и начинает спамить наш бэкенд мусорными запросами. Но обо всем по порядку.

Когда я только начинал строить Солвит, я понимал, что аудиторией сайта будут в основном айтишники и разработчики, которые с удовольствием поковыряют мою апишку. К тому же последнее, с чем я хотел разбираться — это атаки мамкиных хацкеров на мою платформу для подготовки к собеседованию.

Так вот, первая линия обороны в нашем случае — это веб-сервер nginx, в котором стоит ограничение на количество запросов с одного айпишника. Это прописывается прямо в конфиге nginx.conf и выставляется отдельно для GET и POST запросов. Мы разрешаем около 30 GET запросов в секунду, и около 5 POST запросов в секунду. Это позволяет комфортно пользоваться платформой, даже при быстром переключении между основными разделами сайта.

Помимо защиты от большого количества запросов, есть еще один вид атаки — медленные запросы. Это когда юзер сидит на 3G интернете, и каждый запрос длится по 10-15 секунд вместо обычных 50-100 мс. Причем в реальности, тех, кто готов ждать ответа по 10 секунд крайне мало, обычно таким промышляют всякие чудики-злоумышленники. На такой случай в nginx тоже можно поставить ограничение — таймаут, чтобы веб-сервер не захлебнулся запросами и отсекал медленные соединения по таймауту в, например, 10 секунд.

Следующий уровень ограничения хранится уже внутри самого приложения. В нашем случае, часть эндпоинтов на FastAPI используют кастомную зависимость для ограничения количества запросов. Выглядит это довольно лаконично:
@router.post(
"/coding/run",
dependencies=[Depends(coding_run_limit)], # <-- вот
)
async def get_coding_result(...):

Внутри зависимости скрыта логика проверки количества запросов по всем ip-адресам за последние 5 секунд. Суть в том, что часть эндпоинтов исполняют тяжелый и долгий код, поэтому здесь нужно довольно сильное ограничение. Например, мы запускаем код пользователя в изолированной среде на другом сервере, запускаем все тесты, и ждем вебхук-уведомления от сервера с результатами запуска пользовательского кода. Это затратно по времени и ресурсам, поэтому с каждого ip-адреса можно отправлять ограниченное количество запросов.

Изначально мы не вводили такое ограничение, но со временем выяснился занимательный факт:
часть пользователей, когда не могут справиться с решением задачи, начинают атаковать кнопку с отправкой своего кода и штурмуют бэкенд запросами.

Как итог, мы еще и на фронте забацали ограничение, чтобы пока обрабатывается текущий код, нельзя было прислать новый. Вот такой способ борьбы с подгоревшими попками) Но я их понимаю, сам бываю таким, когда не получается решить задачу 😅

С технической точки зрения, мы храним в Redis ключ формата rateLimit:<ip_address> и значение — количество запросов за последние 5 секунд. Если число превышает ограничение на бэке, то юзер получает 429 ошибку a.k.a. "иди остынь". Еще один плюс использования Redis в том, что в нем есть встроенная логика очистки значения через определенное время (TTL), чтобы не заниматься очисткой самим на бэкенде. То есть спустя 5 секунд юзер снова может отправлять запросы на проверку своего кода на бэк.

А знаете, что самое классное? Скоро выложу видео с написанием собственного ограничителя запросов с использованием Redis. Можно будет брать и сразу использовать в своих проектах. Ну круто же?)

И многие из вас даже не знали о существовании одной супер удобной структуре данных в Redis) Ждете? Поставьте много огонечков 🔥
🔥 247
31
👍 16
emoji 9
😁 3
35 120 11.7K

Обсуждение 35

Обсуждение не доступно в веб-версии. Чтобы написать комментарий, перейдите в приложение Telegram.

Обсудить в Telegram