Skip to main content
За лаштунками

Що виявив аудит нашої власної платіжної системи

Дві критичні знахідки, і жодна не була там, де ми б шукали. Відкритий звіт про помилки, які відтоді виправлено.

Опубліковано автор AlphaCloud4 хв читання

У серпні 2026 року ми перевірили нашу платіжну систему від самого фундаменту. Не тому, що щось сталося, а тому, що вона запрацювала незадовго до того, і ми хотіли знати, що пропустили.

Пропустили ми чимало. Ця стаття називає знахідки. Усе описане тут виправлено, і більшість із цього так чи так уже була в публічному списку змін.

Помилка моделювання, з якої виросли обидві критичні знахідки

Коли хтось купує в нас Premium, у нашого платіжного провайдера створюється запис клієнта. Ми зберігали цей запис за ID сервера, адже Premium зрештою потрапляє на сервер.

Це якраз хибна модель, і саме вона стала коренем двох найсерйозніших знахідок.

Запис клієнта належить покупцеві. Він містить його спосіб оплати, історію рахунків і, щойно хтось платить за два сервери, кілька підписок. Якщо прив'язати його до ID сервера, кілька людей вказують на один і той самий рядок.

Знахідка 1: чужі дані клієнта можна було перезаписати без оплати

Купувати може будь-який учасник сервера, а функція, яка знаходить або створює запис клієнта, виконується до оплати. Вона знаходила рядок за ID сервера й мовчки переписувала його на нового потенційного покупця, включно з його адресою електронної пошти в платіжного провайдера.

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

Виправлено перевіркою перед будь-яким повторним використанням, чи запис справді належить цьому акаунту. У разі розбіжності створюється окремий, а інший залишається недоторканим.

Знахідка 2: портал оплати показував чужі підписки

Та сама помилка моделювання, на рівень далі. Перевірка власника стосувалася підписки сервера, але сесія порталу відкривалася для запису клієнта, який за задумом може охоплювати кілька серверів.

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

Виправлено тим, що запис клієнта тепер визначається з конкретної покупки, а не з рядка сервера.

Дві знахідки, де код суперечив нашим власним умовам

Ця категорія турбувала нас найбільше, бо вона не технічна.

Чорний список забирав оплачений час. Наші умови кажуть, що підписка закінчується наприкінці поточного строку. Але під час блокування сервера код викликав негайне скасування: без повернення коштів і без надання послуги з цього моменту за вже оплачений період. Код робив не те, що ми письмово пообіцяли. Тепер оплачений період добігає кінця.

Відмову від права на відмову від договору не можна було довести. Перед покупкою треба підтвердити, що послуга починається негайно. Це підтвердження жило лише в браузері, ніколи не надсилалося й ніде не зберігалося. Тягар доведення лежить на нас, а доказу в нас просто не було. Тепер згода є обов'язковим полем маршруту оформлення покупки, зберігається з часовою позначкою та версією формулювання, а саме формулювання версіоноване: наявний текст ніколи не редагують, завжди створюють нову версію, інакше старі записи вже неможливо було б відтворити.

Другий раунд був гіршим за перший

Після перших 21 знахідки ми подивилися ще раз за кілька тижнів. Це було правильне рішення, бо найсерйозніша знахідка виявилася лише тоді.

Повернення коштів на сервері B видаляло оплачену довічну ліцензію сервера A. Сповіщення про повернення коштів і чарджбеки називають лише платіж, а не сервер. Ми визначали їх через запис клієнта. Після того як у першому раунді ми правильно встановили, що запис належить покупцеві в кількох серверах, саме це визначення вже не було однозначним. Хтось, хто заплатив за два сервери й отримав повернення за один, міг утратити довічну ліцензію іншого.

Тепер визначення проходить чотири точні кроки, а все, що не можна визначити однозначно, повідомляється, а не вгадується.

Виграний чарджбек нічого не відновлював. Під час чарджбеку код видаляв рядок покупки, єдиний доказ того, що цей сервер колись мав довічну ліцензію. Пізніший виграш справи вже нічого не міг повернути. Тепер він позначає, а не видаляє.

Зберігання даних видаляло оплачені сервери. Коли бот залишає сервер, його дані видаляються через 30 днів. Це очищення ніколи не запитувало, чи до сервера прив'язана активна підписка.

Що ми з цього винесли

Баги були не в платіжному коді. Жодна критична знахідка не стосувалася суми, валюти чи обчислення. Усі вони були в моделі за цим: кому належить запис клієнта, кому належить платіж, кому належить сервер. Хто перевіряє платіжну систему, дивлячись на суми, дивиться повз небезпечну частину.

Аудиту потрібен другий раунд. Найсерйозніша знахідка була прямим наслідком виправлення з першого. Це не випадковість: виправлення моделі знецінює припущення, які деінде ще діють.

«Так написано в наших умовах» це речення, яке можна перевірити. Дві знахідки були розбіжностями між тим, що ми письмово пообіцяли, і тим, що робив код. Це можна прочитати, і ніхто цього не робить, бо це схоже на право, а не на інженерію.

Повні умови покупки є в наших умовах використання, право на відмову від договору в розділі відмова від договору.

Інші статті