Есть особый сорт багов: они не вызывают сбоев, не светятся в логах, не ломают ничего видимого. Снаружи все работает — просто не так, как все думают.
Один такой мы нашли в Course.Tours (кейс на сайте). Это образовательный маркетплейс с концепцией «обучение + путешествие». По сути Booking.com для учебы: пользователь выбирает языковую школу в Дубае или летний лагерь в Дублине, бронирует курс, а вместе с ним проживание, трансфер и получает визовую поддержку. Каталог за 30 тысяч курсов офлайн и онлайн, аудитория — сотни тысяч студентов, старт на рынках России, СНГ и ОАЭ с прицелом на остальной мир.
Проект был формально готов к запуску, но при тестировании фаундер видел, что система ведет себя нестабильно: базовые сценарии работали, однако в проде появлялись непредсказуемые сбои. Стало понятно, что нужна внешняя экспертиза. До подключения команды MWI сайт год разрабатывала другая аутсорс-команда. В итоге получился продукт, который выглядел готовым, но при более внимательной проверке оказалось, что часть важных процессов реализована лишь формально.
Разбираться в платежной логике мы начали уже в процессе аудита — и именно тогда наткнулись на проблему. Система засчитывала успешную оплату сразу после нажатия кнопки, даже не проверяя, поступили ли деньги на самом деле.
Must-have кейс для продактов и фаундеров, у которых в продукте заложен прием платежей — особенно в криптовалюте и через рассрочку BNPL. Баг, о котором пойдет речь типовой. Такие уязвимости либо находят случайно, либо уже после финансовых потерь.
Логика крипто-чекаута в исходном продукте была устроена так: пользователь выбирал в какой именно криптовалюте он хочет заплатить (например, USDT) и через какую блокчейн-сеть пройдет перевод. После этого открывалось окно с реквизитами: сумма к оплате, адрес кошелька, на который нужно отправить деньги, QR-код и таймер обратного отсчета — сколько времени остается на перевод. Под ним две кнопки: «Отменить» и «Продолжить». И вот кнопка «Продолжить» переводила заказ в статус «Платеж принят».
И все. Никакой связи с блокчейном или проверки, действительно ли пришли деньги и сколько. Кнопка просто меняла статус в базе, а фронтенд имитировал оплату.
Чтобы воспользоваться этой уязвимостью, не нужно было ничего взламывать или разбираться в крипте. Достаточно один раз заметить закономерность: после «Отмены» доступ к курсу не появляется, а после «Продолжить» — появляется сразу. Дальше все сводилось к простой механике: нажимаешь кнопку, получаешь статус «оплачено» и доступ к любому курсу из каталога, включая варианты по $2k.
Уязвимость не замечали именно потому, что внешне все выглядело нормально: Stripe подключен, крипто-оплата есть, кнопки нажимаются, статусы обновляются. Для пользователя и для бизнеса система выглядела рабочей.
При полном разборе платежного модуля фейковая кнопка — лишь самое очевидное проявление более глубокой проблемы. Масштаб проекта изначально предполагал десятки сценариев оплаты под разные рынки: валюты, международные карты, криптоплатежи, рассрочка, чеки на тысячи долларов. Чем больше в платежной системе сценариев, тем больше в ней, как оказалось, скрытых проблем. Аудит длился 9 часов и выявил 17 уязвимостей, которые удалось сгруппировать в 4 слоя, так как каждый «ломал» систему по-своему. И вот что получилось:
Кнопка «Продолжить» — самый яркий случай в этой группе, но не единственный:
Следующий слой — потери на конверсии. Ситуации, при которых сайт терял пользователей, готовых оплатить курс:
3. Проблемы, которые проявлялись в нестандартных сценариях
Здесь начинались самые болезненные сбои — в ситуациях, когда платеж проходил не идеально. На практике именно они случаются чаще всего:
И поверх всего этого отсутствовал еще один слой базовых функций:
17 проблем. Год в проде. И визуально платежные модули выглядели как «ну да, оплата работает».
Фронтенд продукта видно сразу: экран, кнопка, статус «оплачено», письмо после оплаты. Именно это показывают на демо и именно это проверяет заказчик. А то, как система сверяет сумму, подтверждает транзакцию, обрабатывает дубль или недоплату, на скриншоте не покажешь. Кнопка, за которой стоит проверка блокчейна, внешне ничем не отличается от кнопки, которая меняет статус в базе. Разница видна позже — когда кто-то целенаправленно изучает платежи или когда цифры в отчетах перестают сходиться.
Владелец продукта не обязан разбираться в этих деталях. Для него логика простая: оплата прошла, доступ выдан, значит, система работает. Но прием денег — это не интерфейс, а цепочка состояний: заказ создан, платеж ожидается, деньги поступили, сумма сверена, курс зафиксирован, доступ выдан. Стоит выпасть одному звену — и продукт живет в режиме «вроде работает, пока никто не посмотрит внимательно».
Здесь и проходит граница между подрядчиком и партнером: подрядчик закрывает задачу по ТЗ, партнер идет дальше и сам поднимает вопросы, которые заказчик не обязан задавать — что будет при недоплате, при дубле, при обрыве сети, при отказе банка. Почти все серьезные проблемы в таких проектах возникают там, где заранее не продумали сценарии до конца.
Чинить начали не с кода, а с описания сценариев. Сначала зафиксировали текущее состояние — буквально оформили сломанную логику в виде полноценного ТЗ, чтобы клиент, разработчики и QA одинаково понимали: вот что сейчас и вот почему это опасно. Дальше действовали по приоритетам.
Первым делом — настроили верификацию. Статус «оплачено» теперь ставится по факту поступления средств, а не по нажатию кнопки. Для крипты это мониторинг адреса с проверкой суммы и фиксацией курса на момент платежа.
Переименовали и переосмыслили кнопку оплаты. Вместо обезличенного «Продолжить» появилась кнопка «Я отправил платеж» с подтверждающим окном «Вы точно отправили?». Теперь пользователь явно подтверждает факт отправки денег. Он больше не может случайно пройти этот шаг, не понимая, что именно подтверждает. В результате число случайных нажатий сократилось примерно вдвое.
Настроили модель с актуальными статусами вместо «оплачено / не оплачено». Теперь у заказа есть состояния под реальные ситуации: ожидание подтверждения, ошибка, просрочка таймера, недоплата. Под каждое определили, что видит пользователь и какие действия должна предпринимать поддержка.
Убрали возможность двойного клика: после первого нажатия кнопка блокируется, появляется индикатор загрузки, и один заказ гарантированно остается одним заказом. Плюс заложили rate-limiting на создание заказов.
Описали уведомления. Для каждого триггера — создание заказа, ожидание, оплата / оплата не прошла — есть текст письма.
И добавили то, чего не было: сообщение-подсказку об ошибке от платежного шлюза NowPayments: «при сумме меньше $2 оплата только картой», заготовку под ручную проверку платежей администратором и аналитику воронки — сколько пользователь хотел оплатить, сколько оплатил, на каком шаге ушел.
Параллельно обнаружились еще несколько уязвимостей того же сорта. Например, ссылка на онлайн-урок оставалась активной после окончания занятия, то есть доступ к контенту тоже фактически был бесконечным и бесплатным. Пофиксили ее так же: токенизация, деактивация после урока, одноразовые ссылки на запись. Принцип везде один: проверять надо факт действия, а не намерение.
Если в продукте есть платежи, вот пять вопросов, которые стоит задать команде заранее:
Ни один из этих вопросов не про код. Все они про продукт. Поэтому о них легко забыть до запуска — пока кто-нибудь случайно не нажмет «Продолжить», не заплатив.
Полный кейс описали на сайте. Переходите, пишите вопросы - поможем найти точки роста и усилить безопасность вашего web-проекта.