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

Подпись вебхука проверяют до разбора JSON и обработки события: если сначала прочитать тело запроса как объект, а потом собрать его заново, исходные байты могут измениться — и проверка HMAC не пройдёт. Ниже — практические подходы, пример для Node.js 22 и меры против повторной доставки; формат заголовков у каждого провайдера нужно сверять отдельно.
Сначала выясните, что именно подписывает отправитель
У вебхуков нет единого формата подписи. Один сервис передаёт HMAC и время в заголовках, другой использует собственный набор заголовков, а третий предлагает проверку через SDK. Нельзя взять алгоритм из чужой инструкции и предположить, что он подойдёт вашей площадке.
До реализации найдите в документации отправителя четыре вещи: алгоритм подписи, точное содержимое подписываемых данных, формат и кодировку подписи, а также способ передать секрет или публичный ключ. Проверьте и то, подписывает ли отправитель время и уникальный идентификатор события. Эти детали определяют код проверки.
Подписанное содержимое нельзя произвольно нормализовать. Пробелы в JSON, порядок полей и переводы строк могут повлиять на результат. Поэтому приложению нужен оригинальный массив байтов тела запроса, а не объект после JSON-парсера.
Термин — HMAC. Это код аутентификации сообщения: отправитель и получатель вычисляют подпись с помощью общего секретного ключа и данных запроса. HMAC помогает обнаружить подмену, но сам по себе не шифрует содержимое запроса.
Три подхода и цена каждого
| Подход | Когда выбирать | За что платите |
|---|---|---|
| SDK отправителя | Провайдер официально поддерживает ваш язык и библиотеку | Меньше ручной работы, но нужно следить за версией SDK и его форматом проверки |
| HMAC по документации | Известны алгоритм, подпись и точный состав данных | Контроль над реализацией; легко ошибиться с байтами, кодировкой или сравнением |
| Асимметричная подпись | Провайдер выпускает подписи, которые проверяются публичным ключом | Секрет подписи не хранится у получателя, но нужно правильно обновлять ключи и проверять их источник |
Если провайдер даёт официальный SDK для вашей версии Node.js, начинайте с него: он обычно уже знает формат заголовков и правила подписи. Если SDK нет или вы пишете интеграцию с собственным отправителем, HMAC можно реализовать напрямую. Не заменяйте один способ другим, если документация явно требует конкретный протокол.
Подпись отвечает на вопрос «эти данные созданы владельцем ключа и не изменились?». Она не отвечает на вопрос «обрабатывали ли мы это событие раньше?».
HMAC в Node.js 22: проверяйте исходные байты
Ниже — пример для собственного формата, а не универсальный стандарт вебхуков. Предполагается, что отправитель передаёт время в секундах и шестнадцатеричную подпись HMAC-SHA256, вычисленную от строки времени, точки и исходного тела запроса. Для реального провайдера замените формат ровно на тот, который описан в его документации.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebhook({
rawBody,
timestamp,
signatureHex,
secret,
now = Date.now(),
toleranceSeconds = 300,
}) {
if (!Buffer.isBuffer(rawBody)) return false;
if (!/^\d+$/.test(timestamp)) return false;
if (!/^[a-f0-9]{64}$/i.test(signatureHex)) return false;
const timestampSeconds = Number(timestamp);
if (!Number.isSafeInteger(timestampSeconds)) return false;
if (Math.abs(Math.floor(now / 1000) - timestampSeconds) > toleranceSeconds) {
return false;
}
const signedPayload = Buffer.concat([
Buffer.from(`${timestamp}.`, 'utf8'),
rawBody,
]);
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest();
const received = Buffer.from(signatureHex, 'hex');
return received.length === expected.length
&& timingSafeEqual(received, expected);
}
Функция использует криптографические примитивы встроенного модуля Node.js 22. Она не читает HTTP-запрос и не извлекает заголовки: передайте ей исходное тело, значение времени, подпись и секрет, полученные по правилам конкретного провайдера. Не подставляйте секрет в исходный код — храните его в системе управления секретами или защищённой конфигурации.
Сравнение выполняется только после проверки длины подписи: функция timingSafeEqual требует, чтобы буферы имели одинаковую длину. Окно в пять минут в примере — настраиваемое значение, а не универсальное требование. Его нужно согласовать с документацией отправителя и особенностями доставки.
Порядок обработки важнее одного вызова HMAC
- Сохраните исходное тело. Настройте HTTP-слой так, чтобы до проверки подписи приложение получило тело как байты. Если middleware уже распарсил JSON, проверьте документацию фреймворка: восстановление исходных байтов через повторную сериализацию ненадёжно. Проверка проходит, если тест с корректной подписью на исходном теле успешен.
- Проверьте формат и свежесть. Разберите заголовки согласно документации отправителя, отклоните некорректную подпись и проверьте время, если оно входит в протокол. Проверка проходит, если запрос с неверной подписью и запрос за пределами допустимого окна не попадают в обработчик события.
- Проверьте подпись до бизнес-логики. Не создавайте заказ, не меняйте баланс и не запускайте задачу до успешной проверки. Проверка проходит, если подмена одного байта в теле приводит к отказу без побочных эффектов.
- Защититесь от повторной обработки. Используйте идентификатор доставки или события, если отправитель его предоставляет, и сохраняйте обработанные значения с ограничением уникальности. Проверка проходит, если повтор того же события не создаёт вторую операцию.
- Подтвердите приём только после надёжной фиксации. Если обработка долгая, сохраните проверенное событие в очередь или хранилище и затем ответьте отправителю. Конкретные коды ответа и правила повторной доставки сверяйте с документацией провайдера. Проверка проходит, если событие не теряется при временном сбое обработчика.
Что ломается на практике
Тело прочитали как JSON и собрали заново
На вид объект тот же, но байтовое представление может отличаться: например, сериализатор убрал пробелы или поменял формат числа. HMAC считают по байтам, а не по смыслу JSON. Решение — получить raw body до парсинга и передавать в функцию именно его.
Считают временную метку защитой от всех повторов
Свежий подписанный запрос можно повторить ещё раз, пока он укладывается в разрешённое окно. Временная метка ограничивает срок пригодности подписи, но не гарантирует однократную обработку. Для этого нужна дедупликация по идентификатору события и атомарная запись в хранилище.
Секрет или ключ обновили без переходного периода
При ротации провайдер и ваше приложение могут некоторое время использовать разные ключи. Если сервис поддерживает несколько активных ключей или период перекрытия, предусмотрите проверку по актуальному набору согласно его документации. Не принимайте подпись без проверки только ради бесшовного переключения.
Отправителю возвращают успех до сохранения события
Если сервер ответил об успешном приёме, а затем упал до записи в очередь, отправитель может не прислать событие повторно. Сначала надёжно сохраните проверенное событие, затем подтверждайте приём. Внешние эффекты — например, списание или отправку письма — также должны учитывать повторную доставку.
Когда какой подход выбирать
- Есть официальный SDK. Используйте его, если он поддерживает вашу среду и позволяет проверить подпись до обработки тела. Зафиксируйте версию зависимости и добавьте тест по официальному примеру провайдера.
- Формат простой и полностью описан. Реализуйте проверку HMAC стандартными криптографическими функциями платформы, как в примере. Покройте тестами корректное тело, изменённый байт, неверную подпись и истёкшее время.
- Провайдер использует публичные ключи. Следуйте его процедуре проверки и обновления ключей; не заменяйте асимметричную подпись самодельным HMAC.
В продакшене не записывайте секреты и полное содержимое чувствительных вебхуков в логи. Фиксируйте результат проверки, идентификатор события и безопасные диагностические данные. Отдельно контролируйте долю отказов подписи, задержку до сохранения события и число повторов: всплеск ошибок может означать не атаку, а неверно настроенное чтение тела или смену ключа.
Что сделать дальше
Возьмите документацию именно вашего отправителя и зафиксируйте формат подписи, состав подписываемых данных и правила ротации ключей. Затем добавьте тесты на исходное тело, подмену данных, устаревшую метку времени и повтор события. Перед выпуском проверьте в тестовой среде, что приложение сохраняет событие до ответа об успешном приёме.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: Node.js 22: документация модуля crypto, GitHub Docs: проверка подлинности доставок вебхуков
Частые вопросы
Можно ли проверить подпись после JSON.parse?
Достаточно ли проверить временную метку, чтобы остановить повторы?
Какое окно свежести поставить?
Можно ли сделать один формат подписи для всех вебхуков?
Что делать, если подпись перестала проходить после смены ключа?
Нужно ли шифровать тело вебхука, если оно подписано?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.