Кнопка оплаты криптой без верификации: как найти баг и получать курсы бесплатно

2026-06-02 12:40:03 Время чтения 14 мин 221

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

Один такой мы нашли в Course.Tours (кейс на сайте). Это образовательный маркетплейс с концепцией  «обучение + путешествие». По сути Booking.com для учебы: пользователь выбирает языковую школу в Дубае или летний лагерь в Дублине, бронирует курс, а вместе с ним проживание, трансфер и получает визовую поддержку. Каталог за 30 тысяч курсов офлайн и онлайн, аудитория — сотни тысяч студентов, старт на рынках России, СНГ и ОАЭ с прицелом на остальной мир. 

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

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

Must-have кейс для продактов и фаундеров, у которых в продукте заложен прием платежей — особенно в криптовалюте и через рассрочку BNPL. Баг, о котором пойдет речь типовой. Такие уязвимости либо находят случайно, либо уже после финансовых потерь.

Кнопка «Продолжить» вместо проверки денег

Логика крипто-чекаута в исходном продукте была устроена так: пользователь выбирал в какой именно криптовалюте он хочет заплатить (например, USDT) и через какую блокчейн-сеть пройдет перевод. После этого открывалось окно с реквизитами: сумма к оплате, адрес кошелька, на который нужно отправить деньги, QR-код и таймер обратного отсчета — сколько времени остается на перевод. Под ним две кнопки: «Отменить» и «Продолжить». И вот кнопка «Продолжить» переводила заказ в статус «Платеж принят».

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

Чтобы воспользоваться этой уязвимостью, не нужно было ничего взламывать или разбираться в крипте. Достаточно один раз заметить закономерность: после «Отмены» доступ к курсу не появляется, а после «Продолжить» — появляется сразу. Дальше все сводилось к простой механике: нажимаешь кнопку, получаешь статус «оплачено» и доступ к любому курсу из каталога, включая варианты по $2k.

Уязвимость не замечали именно потому, что внешне все выглядело нормально: Stripe подключен, крипто-оплата есть, кнопки нажимаются, статусы обновляются. Для пользователя и для бизнеса система выглядела рабочей. 

17 багов, которые прятались за рабочим фронтэндом

При полном разборе платежного модуля фейковая кнопка — лишь самое очевидное проявление более глубокой проблемы. Масштаб проекта изначально предполагал десятки сценариев оплаты под разные рынки: валюты, международные карты, криптоплатежи, рассрочка, чеки на тысячи долларов. Чем больше в платежной системе сценариев, тем больше в ней, как оказалось, скрытых проблем. Аудит длился 9 часов и выявил 17 уязвимостей, которые удалось сгруппировать в 4 слоя, так как каждый «ломал» систему по-своему. И вот что получилось:

1. Проблемы, из-за которых платформа теряла деньги

Кнопка «Продолжить» — самый яркий случай в этой группе, но не единственный:

  1. Система не проверяла фактическую сумму платежа — деньги могли прийти хоть на один, хоть на сто долларов меньше положенного, а заказ все равно считался оплаченным;
  2. Система не сохраняла курс конвертации и время его фиксации. Для криптовалют это критично: даже через несколько минут одна и та же сумма в USDT может соответствовать другой сумме в долларах или евро. Без этих данных невозможно корректно сверить платеж и понять, полностью ли он был оплачен;
  3. Один и тот же заказ можно было создать несколько раз двойным кликом;
  4. Ограничений на частоту создания заказов тоже не было — ни по IP, ни по аккаунту, поэтому генерировать их можно было пачками.

2. Проблемы, из-за которых терялись пользователи

Следующий слой — потери на конверсии. Ситуации, при которых сайт терял пользователей, готовых оплатить курс:

  1. кнопка называлась «Продолжить», а не «Я оплатил», и пользователь не понимал, что на каком этапе находится;
  2. таймер в окне крипто-оплаты тикал, но нигде не объяснялось, что произойдет после его окончания: заказ отменится, сумма пересчитается или не случится ничего;
  3. система никак не предупреждала про комиссию блокчейн-сети: пользователь отправлял ровно ту сумму, которую видел на экране, сеть удерживала комиссию, и до платформы доходило меньше;
  4. на чекауте висел блок с адресом для выставления счета и обязательными полями, которые большинству пользователей были не нужны и только резали конверсию. 

3. Проблемы, которые проявлялись в нестандартных сценариях

Здесь начинались самые болезненные сбои — в ситуациях, когда платеж проходил не идеально. На практике именно они случаются чаще всего:

  1. у системы не было статусов: ошибка банка, ожидание подтверждения сети, истекший таймер, частичная оплата — все сводилось к примитивному «успех / неуспех»;
  2. не были продуманы email-рассылки в ответ на действия пользователя: студент мог оплатить курс и не получить ни одного уведомления от сайта;
  3. у поддержки не было ни интерфейса для проверки платежей, ни сценариев для кейсов вроде «я оплатил, а доступа нет»;
  4. недоплаты, переплаты и частичные платежи — обычная история для крипты — системой не учитывались.

4. То, чего в платежном модуле не было

И поверх всего этого отсутствовал еще один слой базовых функций:

  1. ручная проверка крипто-платежей на случай, если алгоритм ошибется;
  2. возможность выставить счет юридическому лицу;
  3. аналитика платежной воронки — понять, на каком шаге пользователи бросают оплату, было невозможно.

17 проблем. Год в проде. И визуально платежные модули выглядели как «ну да, оплата работает».

Почему появляются такие баги, когда продукт отдают на аутсорс

Фронтенд продукта видно сразу: экран, кнопка, статус «оплачено», письмо после оплаты. Именно это показывают на демо и именно это проверяет заказчик. А то, как система сверяет сумму, подтверждает транзакцию, обрабатывает дубль или недоплату, на скриншоте не покажешь. Кнопка, за которой стоит проверка блокчейна, внешне ничем не отличается от кнопки, которая меняет статус в базе. Разница видна позже — когда кто-то целенаправленно изучает платежи или когда цифры в отчетах перестают сходиться.

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

Здесь и проходит граница между подрядчиком и партнером: подрядчик закрывает задачу по ТЗ, партнер идет дальше и сам поднимает вопросы, которые заказчик не обязан задавать — что будет при недоплате, при дубле, при обрыве сети, при отказе банка. Почти все серьезные проблемы в таких проектах возникают там, где заранее не продумали сценарии до конца.

Что мы поменяли

Чинить начали не с кода, а с описания сценариев. Сначала зафиксировали текущее состояние — буквально оформили сломанную логику в виде полноценного ТЗ, чтобы клиент, разработчики и QA одинаково понимали: вот что сейчас и вот почему это опасно. Дальше действовали по приоритетам.

Первым делом — настроили верификацию. Статус «оплачено» теперь ставится по факту поступления средств, а не по нажатию кнопки. Для крипты это мониторинг адреса с проверкой суммы и фиксацией курса на момент платежа.

Переименовали и переосмыслили кнопку оплаты. Вместо обезличенного «Продолжить» появилась кнопка «Я отправил платеж» с подтверждающим окном «Вы точно отправили?». Теперь пользователь явно подтверждает факт отправки денег. Он больше не может случайно пройти этот шаг, не понимая, что именно подтверждает. В результате число случайных нажатий сократилось примерно вдвое. 

Настроили модель с актуальными статусами вместо «оплачено / не оплачено». Теперь у заказа есть состояния под реальные ситуации: ожидание подтверждения, ошибка, просрочка таймера, недоплата. Под каждое определили, что видит пользователь и какие действия должна предпринимать поддержка.

Убрали возможность двойного клика: после первого нажатия кнопка блокируется, появляется индикатор загрузки, и один заказ гарантированно остается одним заказом. Плюс заложили rate-limiting на создание заказов.

Описали уведомления. Для каждого триггера — создание заказа, ожидание, оплата / оплата не прошла — есть текст письма. 

И добавили то, чего не было: сообщение-подсказку об ошибке от платежного шлюза NowPayments: «при сумме меньше $2 оплата только картой», заготовку под ручную проверку платежей администратором и аналитику воронки — сколько пользователь хотел оплатить, сколько оплатил, на каком шаге ушел.

Параллельно обнаружились еще несколько уязвимостей того же сорта. Например, ссылка на онлайн-урок оставалась активной после окончания занятия, то есть доступ к контенту тоже фактически был бесконечным и бесплатным. Пофиксили ее так же: токенизация, деактивация после урока, одноразовые ссылки на запись. Принцип везде один: проверять надо факт действия, а не намерение.

Пять вопросов к чекауту продукта перед запуском

Если в продукте есть платежи, вот пять вопросов, которые стоит задать команде заранее: 

  1. Каким образом подтверждается факт оплаты? Если ответ звучит как «пользователь нажал кнопку» или «провайдер вернул редирект» — это не подтверждение оплаты. Система должна проверять, что деньги действительно поступили и что сумма совпадает с ожидаемой.
  2. Что произойдет, если нажать кнопку оплаты дважды? Если в этот момент можно создать два заказа — проблема выявлена, просто пока ее не увидели в проде.
  3. Какие статусы есть у заказа, кроме «оплачено» и «не оплачено»? Если только эти два, то система не различает ошибку банка, ожидание подтверждения, просроченный платеж или недоплату. А значит, поддержка в любой нестандартной ситуации работает без фактических данных.
  4. Какой отклик получает пользователь на каждом этапе оплаты? Если после части действий не приходит вообще ничего, это почти гарантированно превращается в потерю конверсии, и увеличивает поток обращений в поддержку.
  5. Что делает поддержка, когда пользователь пишет «я оплатил, а доступа нет»? Если у команды нет интерфейса для проверки платежа и понятного сценария действий, каждый такой кейс приходится разбирать вручную. Часть из них в итоге заканчивается потерянными деньгами или пользователями.

Ни один из этих вопросов не про код. Все они про продукт. Поэтому о них легко забыть до запуска — пока кто-нибудь случайно не нажмет «Продолжить», не заплатив.

Полный кейс описали на сайте. Переходите, пишите вопросы - поможем найти точки роста и усилить безопасность вашего web-проекта.