Безопасность платежей на сайте: подпись запросов, журнал, защита от подделки
Безопасность платежей на сайте держится на четырёх вещах: проверка подписи каждого входящего уведомления, определение суммы и статуса только на сервере, журнал всех обращений и защита от повторов. Карточные данные при этом не должны попадать на ваш сервер вовсе: ввод происходит на стороне платёжного провайдера. Если эти принципы соблюдены, подделать «успешную оплату» становится значительно сложнее.
Ниже разбор по порядку: от того, какие атаки реальны для интернет-магазина или сервиса с оплатой, до конкретных приёмов в коде. Точные алгоритмы подписи у каждого провайдера свои, поэтому их нужно брать из его документации.
Какие угрозы реальны для платёжной интеграции
Не нужно строить защиту от всего. Для типичного сайта с онлайн-оплатой основные риски такие:
- Поддельное уведомление об оплате. Кто-то отправляет на ваш адрес запрос, имитирующий «оплата прошла», и получает товар бесплатно.
- Подмена суммы. Покупатель меняет в форме цену, и заказ оформляется на меньшую сумму.
- Повторное уведомление. Одно и то же подтверждение обрабатывается дважды: баланс начислен два раза, товар отправлен дважды.
- Перебор идентификаторов. Злоумышленник подбирает номера заказов, чтобы увидеть чужие данные или изменить статус.
- Утечка секретов. Ключи провайдера попали в публичный репозиторий, логи или клиентский код.
- Подмена реквизитов или ссылки на оплату через уязвимость самого сайта.
Против каждого из них есть простое средство, и ниже они идут по очереди.
Подпись запроса: как убедиться, что уведомление настоящее
Платёжные системы подписывают свои запросы: добавляют значение, вычисленное по содержимому запроса и общему секретному ключу, либо передают авторизационный заголовок. Ваша задача воспроизвести вычисление и сравнить результат.
Схематично на PHP (иллюстрация принципа, а не формат конкретного провайдера):
$secret = getenv('PAY_SECRET'); // секрет не в коде и не в репозитории
$body = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $received)) {
http_response_code(403);
// записать попытку в журнал, не раскрывая детали ответа
exit;
}
Что важно в этом фрагменте:
- подпись считается от сырого тела запроса, а не от разобранных и заново собранных полей: изменение порядка или пробела ломает проверку;
- используется
hash_equals, а не==, чтобы избежать атак по времени сравнения; - секрет берётся из окружения;
- при ошибке ответ минимален, подробности остаются в журнале.
Если провайдер использует иной механизм, например базовую авторизацию в заголовке или список разрешённых IP-адресов, принцип тот же: проверка идёт первой, до любого обращения к базе. Фильтр по IP-адресам полезен как дополнительный слой, но не заменяет подпись: адреса меняются, а заголовки можно подделать за прокси. Актуальные адреса и способы проверки сверяйте у провайдера.
Сумма и статус определяются только на сервере
Правило, которое снимает целый класс ошибок: ничто, пришедшее из браузера, не определяет цену. Сумма заказа вычисляется на сервере из каталога и корзины. На страницу оплаты передаётся то, что посчитал сервер, и именно эту величину вы сверяете, когда провайдер подтверждает платёж.
Конкретная последовательность проверок в обработчике уведомления:
- Подпись верна.
- Заказ с таким идентификатором существует.
- Сумма в уведомлении равна сумме заказа в вашей базе.
- Валюта совпадает.
- Заказ находится в статусе, из которого допустим переход в «оплачен».
- Платёж с таким идентификатором транзакции ещё не обработан.
Только после шести проверок меняется статус. Любое отклонение оформляется как ошибка по правилам провайдера и записывается в журнал для разбора.
Переход статусов задавайте как явный конечный автомат: допустимые переходы перечислены, остальные запрещены. Это защищает от странных последовательностей, например «отменён, затем оплачен».
Повторы и идемпотентность
Платёжные системы повторяют уведомления, если не получили ответа, и иногда присылают дубли. Обработчик должен давать одинаковый результат при повторе. Практический приём: уникальный индекс на пару «провайдер и идентификатор транзакции» и вставка записи как первое действие в транзакции базы данных. Если вставка не удалась из-за дубля, вы возвращаете ранее зафиксированный результат и не выполняете побочных действий повторно.
Тема подробно разобрана в статье про идемпотентность и повторные уведомления платёжных систем, и я советую прочитать её вместе с этим материалом.
Побочные действия (письма, отправка на склад, начисления) лучше выносить в очередь, а не выполнять прямо в обработчике. Тогда ответ провайдеру возвращается быстро, а сбой почтового сервера не превращается в сбой платежа.
Журнал: что писать и как хранить
Журнал это основа разбора спорных ситуаций. Когда клиент говорит «я заплатил», а сайт не видит оплаты, вам нужны факты, а не предположения. Что стоит записывать:
- время получения запроса и адрес источника;
- сырое тело запроса и ключевые заголовки (без секретов);
- результат проверки подписи;
- идентификатор заказа и транзакции;
- принятое решение и отправленный ответ;
- изменения статусов с указанием причины и источника (уведомление, ручное действие, сверка).
Чего писать нельзя:
- полные данные банковских карт;
- секретные ключи и токены;
- персональные данные сверх необходимого.
Журнал должен быть защищён от изменения задним числом: хотя бы отдельная таблица или файл с ограниченными правами, а лучше внешняя система сбора логов. Определите срок хранения и права доступа, а вопросы хранения персональных данных уточните у юриста, потому что требования зависят от страны и вида деятельности.
Секреты, доступы и инфраструктура
Многие инциденты происходят не из-за кода обработчика, а из-за окружения.
- Ключи провайдера хранятся в переменных окружения или менеджере секретов, а не в файле, который лежит в репозитории.
- Для тестового и боевого режимов используются разные ключи.
- Доступ к панели провайдера защищён двухфакторной аутентификацией; об этом подробнее в материале про пароли и двухфакторную аутентификацию в компании.
- Адрес обработчика работает только по https с корректным сертификатом.
- Сайт не раскрывает сообщения об ошибках с путями файлов и деталями базы.
- Зависимости и CMS обновляются, а административная часть закрыта от посторонних.
- Права на запись в каталоги сайта минимальны.
Отдельный совет: при подозрении на утечку ключа его нужно немедленно перевыпустить в личном кабинете провайдера и обновить в конфигурации. Заранее продумайте, как это сделать без простоя.
Защита страницы оплаты и форм
Форма, где пользователь вводит карточные данные, должна находиться на стороне провайдера: переход на его страницу или встроенная форма, которую он предоставляет. Тогда требования безопасности к вам снижаются, но не исчезают. Проверьте:
- ссылка на оплату формируется на сервере, параметры нельзя подменить из браузера, либо защищены подписью;
- идентификаторы заказов непредсказуемы или доступ к заказу проверяется по владельцу;
- заголовки безопасности настроены (подробнее: безопасность сайта: CSP, HSTS и другие заголовки);
- сторонние скрипты на странице оформления сведены к минимуму, потому что скрипт аналитики, скомпрометированный на странице оплаты, становится вектором атаки.
Мониторинг и сверка
Даже идеальный код не защищает от сбоев в цепочке. Настройте оповещения о событиях, которые указывают на проблему: всплеск ошибок подписи, платежи в статусе «ожидает» дольше обычного, расхождение сумм, резкое падение числа успешных оплат. Раз в день сверяйте ваши данные с выгрузкой провайдера. Если у вас учёт в 1С, подумайте о том, как автоматизировать сверку платежей с 1С.
Частые вопросы
Нужно ли мне проходить сертификацию по стандартам карточной индустрии?
Если карточные данные вводятся на странице провайдера и не проходят через ваш сервер, требования для вас значительно мягче. Точный уровень и обязанности зависят от схемы подключения и условий договора, поэтому уточняйте их у провайдера или банка-эквайера.
Достаточно ли проверять только IP-адрес провайдера?
Нет. Адреса могут измениться, а при некоторых конфигурациях заголовки подменяются. Подпись или иной криптографический механизм провайдера остаётся основной проверкой, фильтр по адресам дополнительный.
Что делать, если пришло уведомление с неверной подписью?
Отклонить запрос, записать в журнал и не менять данные. Единичные случаи могут быть ошибками настройки, но серия запросов повод проверить, не пытается ли кто-то подобрать подпись, и усилить ограничение частоты запросов.
Как проверить свою защиту?
Составьте набор негативных тестов: запрос без подписи, с неверной подписью, с изменённой суммой, повтор, несуществующий заказ, неверный статус. Все они должны быть отклонены или обработаны безопасно. Полный список сценариев приведён в статье про тестирование платёжной интеграции.
Если нужен аудит существующей оплаты или разработка интеграции с нуля, посмотрите страницу об интеграциях платёжных систем или напишите в Telegram.