редакции
Почему наш первый платежный модуль рухнул под хаками и чему это научило маленькую команду

Команда из пяти человек решила быстро собрать минимально рабочий платежный модуль. Сроки поджимали, клиенты ждали. Модуль запустили, он даже работал, пока один вечер не превратился в ад из логов, странных запросов и внезапно пропавших денег. Позже стало понятно: никто никого целенаправленно не атаковал. Просто слабое место нашлось быстрее, чем успели закрыть.
Дальше — разбор реальных технических шагов и тех мелочей, которые в итоге стали критичными.
1. Слишком смелый оптимизм при проектировании
Платежный модуль писался как вспомогательная часть проекта: API для приема платежей, проверка статусов, обработка callback от провайдера. Внутри команды почему-то казалось, что раз объём логики небольшой, то и рисков почти нет. Поэтому:
- API сделали без полноценной аутентификации для внутренних сервисов.
- Использовали старую библиотеку JWT, которая уже не поддерживалась.
- Логи писали в общий каталог, без ротации и без внимания к объему.
Когда расписали архитектуру на доске, она выглядела аккуратно. На деле это была конструкция, которой достаточно было пнуть в бок.
2. Точка входа, о которой никто не вспомнил
Всё сломалось из-за одного параметры запроса, который никто не проверял.
Модуль принимал callback от платежного провайдера с параметром amount. Бэкенд должен был сверять сумму с локальной базой, но проверка так и не появилась. В итоге злоумышленник заметил, что модуль отвечает на запросы без проверки подписи, и начал отправлять кастомные callback. Он не мог вывести деньги, но смог менять статус заказов и накрутить бесплатные продукты.
Сценарий атаки был примитивным:
- Находится открытый URL для callback.
- Подставляется любой order_id.
- Модуль принимает статус success и меняет запись в базе.
Дальше пошло накручивание заказов, и только нагрузка на сервис выдала проблему.
3. Как обнаружили взлом и что было в логах
Первыми заподозрили неладное админы, которые работали в соседнем проекте. Один увидел вывод команды, которая выглядела так, будто API получает сотни похожих запросов подряд с разными order_id.
Когда дошли до логов, там было примерно следующее:
- десятки подряд callback с одинакового IP;
- странные суммы типа 999999;
- статусы success без подписи;
- запросы к тестовым заказам давно закрытой витрины.
Увидев, что база меняется, команда заблокировала endpoint на nginx. Только после этого стало ясно, что модуль вообще не был защищён.
4. Разбор полетов: что оказалось слабее всего
Когда эмоции улеглись, выяснилось: слабое место было не одно. Уязвимость выглядела как цепочка:
- отсутствие проверки подписи callback;
- отсутствие ограничения на количество запросов;
- отсутствие валидации входящих данных;
- отсутствие мониторинга активности;
- отсутствие чёткой схемы обработки ошибок.
И что самое неприятное — каждый считал, что другой этим займётся позже.
5. Что исправили сразу
На экстренном собрании переписали модуль практически с нуля. За два дня смогли закрыть самые критичные дыры:
- внедрили проверку подписи по секретному ключу;
- добавили жесткое соответствие суммы локальным данным;
- включили rate limit на уровне nginx;
- вынесли callback в отдельный закрытый маршрут;
- настроили уведомления на подозрительные запросы.
Параллельно пересмотрели архитектуру всего платежного процесса, включая внутренние вызовы.
6. Итоги аудита кода
После инцидента пригласили стороннего специалиста на экспресс-аудит. Он нашёл ещё несколько неприятных вещей:
- Старая библиотека для JWT хранила ключ в простом config-файле.
- Логирование работало через самописный класс, который мог писать ошибки в тело ответа.
- Несколько обработчиков имели уязвимость к SQL-инъекциям из-за неаккуратной работы с ORM.
- Один из эндпоинтов возвращал слишком подробные ошибки, позволяя определить структуру базы.
Каждая из этих мелочей отдельно не убила бы сервис. Но в сумме получилась идеальная точка входа.
7. Главное, что поняла команда из пяти человек
Ситуация показала неприятную правду: маленькая команда не может позволить себе роскошь «потом сделаем нормально». Любой временный костыль в платежах — это потенциальная уязвимость, которая рано или поздно выстрелит.
Ключевые выводы:
- безопасность должна быть частью планирования, а не украшением в конце;
- никакой упрощённый callback не должен попадать в боевой контур;
- мониторинг важнее, чем кажется, особенно в маленькой инфраструктуре;
- проще сразу сделать неидеально, но безопасно.
Вывод
Первый платежный модуль стал для команды болезненным уроком. Сломали его не потому, что кто-то гениально атаковал систему — а потому, что система сама пригласила гостей внутрь. После этого проекта авторы ещё много раз спорили, где стоит экономить время, а где нет. И каждый раз вспоминали тот вечер, когда пришлось чинить следы чужого творчества в логах.