К содержимому

Проверка подписи вебхука: поиск и устранение ошибок

Как диагностировать сбой проверки подписи вебхука: секрет, сырой body, логи и типичные причины отказа.

Payora10 мин чтенияEN · RU · UK · ES · DE · FR · IT · PL · PT · HI · ZH
Проверка подписи вебхука: поиск и устранение ошибок

Как работает проверка подписи вебхука

Подпись вебхука — это краткое подтверждение того, что событие действительно пришло от платёжного провайдера, а не из случайного POST-запроса. Провайдер подписывает полезную нагрузку общим секретом или приватным ключом, а затем ваше приложение проверяет эту подпись, прежде чем доверять событию. Если проверка не проходит, самый безопасный ответ — 401 или 400 и без обновления заказа.

Звучит просто. На практике — редко.

В реальности проверка подписи защищает от поддельных счетов, фальшивых возвратов и изменённых payload’ов, которые на первый взгляд выглядят вполне легитимно. Вебхук с сообщением «платёж завершён» бесполезен, если его может подделать кто угодно. Для команд, которые используют криптоплатёжный шлюз для e-commerce, эта проверка находится ровно между приёмом оплаты и выполнением заказа, поэтому одна неверная предпосылка может остановить весь процесс.

Большинство провайдеров подписывают либо весь сырой body, либо timestamp, либо и то и другое. Некоторые используют HMAC-SHA256, другие — заголовок с версиями и несколькими значениями подписи. Достаточно одного несовпадения. Провайдер может быть прав, а ваше приложение всё равно отклонит событие, если вы изменили хотя бы один байт.

Сначала убедитесь, что сбой действительно связан с подписью

Логи должны отвечать на этот вопрос меньше чем за 5 минут. Если не отвечают — они недостаточно подробные.

Начните с точного кода ответа и текста ошибки. 400 с сообщением «invalid signature» указывает в одну сторону; 500 или таймаут — в другую. Ошибки доставки, неправильные маршруты, TLS-проблемы и таймауты прокси на панели могут выглядеть как проблемы с вебхуком, но это не одно и то же. Если провайдер показывает статус «delivered», а ваш endpoint возвращает 200, скорее всего, проверка подписи не является причиной сбоя. В таких случаях важно отделить обычный сбой доставки от того, что в логах выглядит как ошибка invalid signature вебхук.

Проверьте три вещи вместе: лог доставки у провайдера, логи приложения и лог обратного прокси, если он у вас есть. Запрос, который вообще не дошёл до вашего кода, не может провалить проверку подписи. Запрос, который дошёл до кода, но вернул 401, — обычно может. Здесь важна одна деталь: если событие есть в логах, но ошибки подписи нет, возможно, ваш код отклоняет payload уже позже, после шага проверки.

Коротко: сначала подтвердите симптом, потом меняйте код. Если сомневаетесь, начните с вопроса: как проверить подпись webhook.

Проверьте общий секрет или ключ подписи

Это самая распространённая ошибка, и она настолько банальна, что от этого опасна. Секрет вебхука в вашем приложении должен в точности совпадать с текущим секретом в панели провайдера оплаты, включая скрытые пробелы, которые могли попасть при копировании из текстового поля. Если провайдер сменил секрет во вторник на прошлой неделе, старый ключ всё ещё будет лежать в вашей среде, пока кто-то его не обновит.

Сравнивайте секрет в панели не с ноутбуком, а с вашей средой развертывания. Production, staging и локальная разработка должны иметь свои значения. Команда, которая переносит тестовый секрет в production, может потратить 2 часа, пытаясь понять причину ошибки подписи, вызванной одной вставленной строкой. Такое часто случается во время запуска и при миграции с одного процессора на другой. Для команд, которые используют криптоплатёжный шлюз для e-commerce, несоответствие секретов часто всплывает сразу после выпуска нового checkout.

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

Восстановите сырой запрос в точности как он был получен

Здесь ломаются многие хорошие реализации. Проверка подписи обычно зависит от точных сырых байтов, которые отправил провайдер, а не от JSON-объекта, который фреймворк создал после парсинга. Если ваш код преобразует числа, меняет порядок полей, убирает пробелы или нормализует окончания строк, вычисленная подпись может измениться, даже если payload «выглядит» идентично.

Некоторые фреймворки читают body один раз и затем удаляют исходный поток. Другие автоматически парсят JSON ещё до того, как отработает middleware. В результате валидатор получает восстановленный объект, а не сырой body. Обычно решение такое: сначала сохранить неизменённую полезную нагрузку, а затем передать именно этот буфер в функцию проверки подписи. Один параметр фреймворка может решить, сохраняется body или переписывается.

Например, payload из 14 полей может прийти в определённом байтовом порядке, который ваш код никогда не увидит, если парсер сортирует ключи. Завершающий перевод строки тоже может иметь значение. Даже одно преобразование UTF-8 может иметь значение. Да, один невидимый символ способен сломать всю проверку.

Если ваш стек это позволяет, логируйте длину body и хэш сырого запроса до парсинга. Не логируйте секреты. Зато логируйте точный путь кода, который читает поток. Так гораздо проще понять, почему как протестировать криптоплатёж иногда проходит в Postman, но падает на реальном вебхуке от провайдера.

Проверьте timestamp, синхронизацию часов и защиту от повторной отправки

Во многих схемах вебхуков используется timestamp, чтобы старые события нельзя было повторно проиграть позже. Ваш валидатор может отклонить запрос, если timestamp выходит за пределы допустимого окна, часто 5 минут или другого лимита, заданного провайдером. Это защищает от replay-атак, но также означает, что системные часы сервера должны быть корректны.

Проверьте системное время на каждом сервере, который обрабатывает вебхуки. Если часы отстают на 3 минуты, подпись может не пройти, даже если payload правильный. Если сервер работает в контейнере, проверьте и время хоста. На машине, которая принимает решение о проверке, должен быть активен NTP или другой сервис синхронизации времени. Сервер с отклонением на 10 минут может превратить корректный вебхук в ложный сбой.

Защита от повторной отправки тоже важна. Если ваш код кэширует уже виденные подписи или ID событий, убедитесь, что TTL кэша совпадает с ожидаемым окном повторов у провайдера. Устаревшая запись в кэше может привести к отклонению корректной повторной доставки. Повторная попытка со стороны провайдера — это не то же самое, что злонамеренный replay, поэтому код должен различать оба случая. Это небольшая деталь с большими последствиями.

Убедитесь, что заголовок подписи парсится правильно

Заголовки подписи не всегда выглядят одинаково. Некоторые провайдеры отправляют один заголовок с одним хэшем, другие — список пар ключ-значение через запятую, метку версии и timestamp. Ваш парсер должен соответствовать именно этому формату. Если он ожидает v1=, а провайдер отправляет t= вместе с несколькими версиями, проверка не пройдёт ещё до начала криптографии.

Следите за ошибками обрезки пробелов. Важны и ведущие, и конечные пробелы. То же относится к ошибкам кодировки, когда фреймворк декодирует заголовки как Latin-1 вместо UTF-8. Лишний перенос строки, попавший в переменную окружения, может сломать сравнение. Не нормализуйте заголовок, если провайдер прямо не разрешает это. Не переводите значение в нижний регистр, если оно должно оставаться бинарно-безопасным. Эти две вещи причиняют больше боли, чем должны.

С несколькими схемами тоже нужно быть аккуратным. Некоторые провайдеры публикуют текущую подпись и предыдущую для переходного периода. Ваш валидатор должен понимать, какую версию он использует, и корректно отклонять неизвестные версии. Если в заголовке есть и timestamp, и подпись, проверяйте оба. Если в документации указан префикс подписи, используйте его в точности. Подробнее о безопасной обработке вебхуков см. лучшая практика безопасности криптоплатежей.

Проверьте на известном корректном событии вебхука

Известно корректное событие экономит время. Отправьте его повторно из панели провайдера или используйте документированный пример payload и сравните ожидаемую подпись с той, что вычисляет ваш код. Если пример проходит проверку, а живой трафик — нет, проблема, скорее всего, в конфигурации, окружении или ротации секрета. Если не проходит ни один вариант, проблема в кодовом пути.

Используйте один event ID и один timestamp. Затем сравните четыре вещи: сырой body, значение заголовка, секрет и часы. Этого достаточно, чтобы изолировать большинство сбоев. Тестовый payload, скопированный в локальный файл, может содержать форматирование, которого ваш реальный endpoint никогда не увидит, поэтому всегда лучше использовать инструмент повторной отправки у провайдера, если он есть. Один resend лучше, чем 20 догадок.

Если провайдер предлагает калькулятор подписи, прогоните через него тот же payload. Если ваш результат отличается хотя бы на один символ, у вас проблема с парсингом или сохранением body. Если результат совпадает, но провайдер всё равно отклоняет запрос, значит, неверны секрет или разбор заголовка. Это разделение сразу подсказывает, где искать дальше.

Это также тот момент, когда staging и production нужно чётко разделить. Событие из staging, отправленное на production-секрет, будет падать каждый раз, и ошибка будет выглядеть точно так же, как сломанная подпись, пока вы не проверите метку окружения.

Укрепите и мониторьте endpoint вебхука

Когда проверка подписи заработает, важно не сломать её снова. Логируйте ID события, результат проверки, имя провайдера и имя окружения. Не логируйте полный секрет. Не логируйте полный сырой body, если в нём есть чувствительные данные клиента. Используйте короткое, понятное сообщение об ошибке для операторов и простой отказ для провайдера.

Ротацию секрета вебхука проводите по расписанию и документируйте передачу. Безопаснее двухэтапный процесс: добавить новый ключ, убедиться, что оба ключа работают, если поддерживается перекрытие, затем удалить старый. Разделяйте тестовые и production-endpoint’ы, чтобы ошибка на staging не затронула живые заказы. Это звучит очевидно; команды всё равно их путают.

Настраивайте alert на всплеск ошибок подписи, а не на единичную неудачную попытку. Один сбой может быть просто неудачным повтором. Пять сбоев за 1 минуту уже требуют внимания. Включите в alert маршрут, провайдера и код ответа, чтобы дежурный быстро понял, в чём проблема: в несовпадении секрета, баге парсера или в проблеме с часами. Если вы ещё настраиваете endpoint, статья как настроить криптоплатежи поможет сразу сделать webhook-путь максимально простым.

И последнее: держите короткий runbook с точными шагами по тому, как исправить неудачную подпись вебхука для криптоплатежа, включая то, где хранится секрет, как повторно отправить одно событие и какая строка лога подтверждает проверку. Runbook на 10 строк экономит час в 2 часа ночи.

Комментарии

Готовы начать?

Создайте аккаунт — и первый счёт заработает меньше чем за час.