Webhook аутентифицируется подписью HMAC-SHA256, которую отправитель вычисляет над меткой времени плюс точным сырым телом, используя общий секрет, и передаёт в заголовке. Получатель заново вычисляет HMAC, сравнивает его за константное время и отклоняет запрос, если подпись не совпадает или метка времени вне окна допуска (защита от повторов).
Почему подписи, а не списки разрешённых IP
Эндпоинт webhook — это публичный HTTPS URL, поэтому выполнить на него POST может кто угодно. TLS доказывает, что клиент достиг вашего сервера; он никак не доказывает, что запрос действительно пришёл с платформы сообщений. Списки разрешённых IP хрупки (IP отправителей меняются, и они не работают за общим egress). Стандартный ответ — код аутентификации сообщения с ключом: отправитель и получатель делят секрет, отправитель подписывает им каждую полезную нагрузку, а получатель проверяет. HMAC (RFC 2104) с SHA-256 (RFC 6234) — повсеместная конструкция, поскольку она быстра, стандартизирована и — в отличие от голого хеша конкатенации секрета с сообщением — доказуемо устойчива к атакам удлинения длины. Валидная подпись доказывает сразу две вещи: полезная нагрузка создана держателем секрета и не была изменена при передаче.
Заголовки подписи и метки времени
Отправитель включает как минимум два заголовка: метку времени (когда запрос был подписан) и подпись. Подписываемое содержимое — это не сам JSON, а каноническая строка, связывающая метку времени с телом, — обычно метка времени, разделитель и сырое тело запроса, — чтобы атакующий не мог перенести валидную подпись на другую полезную нагрузку или воспроизвести её в другое время. Именование варьируется у поставщиков (Webhook-Signature / Webhook-Timestamp или X-Signature / X-Timestamp), но конструкция одна и та же. Подпись обычно в hex или base64 и часто несёт префикс схемы (например, v1,), чтобы секрет можно было ротировать на новую схему, не ломая старые верификаторы.
signed_content = timestamp + '.' + raw_body
signature = HMAC_SHA256(secret, signed_content)
Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b8c9a1d... # hex or base64, scheme-prefixedПроверка по сырому телу
Самая частая ошибка проверки — подписывать повторно сериализованное тело. HMAC вычисляется над точными байтами, поэтому получатель должен проверять по сырому телу запроса ровно так, как оно получено, — до любого разбора и повторного кодирования JSON. Разбор в объект и повторная сериализация переупорядочивают ключи, меняют пробелы и нормализуют числа и Unicode-экранирования, порождая другие байты и несовпадение подписи даже когда ничего не было подделано. На практике это означает захват сырого тела в веб-фреймворке до того, как его поглотит middleware разбора тела, проверку HMAC над этими байтами и лишь затем разбор JSON для использования. Кодировка тоже важна: подписывайте и проверяйте одну и ту же последовательность октетов (UTF-8), а не декодированную строку.
raw = request.raw_body # exact bytes, pre-parse
ts = header('Webhook-Timestamp')
sig = header('Webhook-Signature')
if abs(now() - ts) > TOLERANCE: reject # replay window
expected = hmac_sha256(secret, ts + '.' + raw)
if not constant_time_equal(expected, sig): reject
accept(parse_json(raw))Защита от повторов: допуск метки времени и nonce
Одна лишь подпись не останавливает повтор — захваченный, но валидный запрос можно переслать позже. Этот пробел закрывают два механизма. Во-первых, поскольку метка времени входит в подписываемое содержимое, получатель сверяет её со своими часами и отклоняет всё старше допуска (обычно несколько минут — достаточно широкий, чтобы поглотить расхождение часов и задержку повтора, достаточно узкий, чтобы ограничить окно повтора). Во-вторых, для более строгих гарантий получатель записывает nonce — уникальный event id — на период допуска и отклоняет второй запрос с id, который уже видел. Допуск метки времени ограничивает окно; кэш nonce делает повтор внутри этого окна неэффективным. Заметьте, что это дополняет идемпотентность потребителя, которая обрабатывает легитимные дубликаты, а не злонамеренные повторы.
| Контроль | Защищается от | Настройка |
|---|---|---|
| Отметка времени в подписанном содержимом | Перенос подписи на новое время | Должен быть подписан, а не просто заголовок |
| Окно допуска для временной метки | Отложенное воспроизведение (replay) перехваченного запроса | Несколько минут; баланс между рассинхронизацией часов и окном уязвимости |
| Кэш nonce / event-id | Повторное воспроизведение (replay) в пределах окна допуска | Кэшируйте идентификаторы как минимум на время допуска |
| Сравнение за постоянное время | Побочный канал по времени при проверке подписи | Всегда, независимо от вышеизложенного |
Сравнение за константное время
Сравнение вычисленной и полученной подписей обычной проверкой на равенство утекает информацию: побайтовое сравнение возвращается быстрее при раннем несовпадении, и атакующий, способный измерить это время, может восстановить корректную подпись по одному байту за раз. Поэтому проверка должна использовать сравнение за константное время (не зависящее от длины) — стандартное безопасное по времени сравнение в каждой платформе, — которое всегда просматривает полную длину перед возвратом. Это дёшево и не обсуждается; это разница между схемой подписи, которая надёжна, и той, которую можно взломать достаточным числом запросов с замером времени.
Ротация секрета
Секреты подписи должны быть ротируемыми без потери легитимных webhook на лету. Безопасный паттерн — перекрывающиеся ключи: отправителю выдаётся новый секрет, пока старый остаётся валидным, и во время перекрытия отправитель либо подписывает обоими (выдавая две подписи), либо получатель принимает подпись, совпадающую с любым из секретов. Как только каждый производитель подтверждён на новом секрете, старый выводится из эксплуатации. Префикс схемы на подписи (v1, v2,) делает это наблюдаемым и позволяет заодно провести версионированную смену алгоритма. Размещённый валидатор подписи webhook BroadNet позволяет проверить полезную нагрузку, метку времени и секрет против ожидаемого HMAC без написания кода; механика подписи DLR через HMAC-SHA256 разобрана в библиотеке ответов.
Часто задаваемые вопросы
Как вычисляется подпись webhook?
Отправитель строит каноническую строку, связывающую метку времени с сырым телом (обычно timestamp + '.' + body), вычисляет над ней HMAC-SHA256 с общим секретом и помещает результат в заголовок подписи рядом с меткой времени. Получатель заново вычисляет тот же HMAC и сравнивает. Совпадение доказывает, что полезная нагрузка пришла от держателя секрета и не была изменена.
Почему нужно проверять по сырому телу запроса?
HMAC вычисляется над точными байтами. Если разобрать JSON и повторно сериализовать его, порядок ключей, пробелы, форматирование чисел и Unicode-экранирования меняются, поэтому байты отличаются и подпись не совпадает даже когда ничего не подделано. Захватите сырое тело до middleware разбора тела, проверьте, затем разбирайте.
Как допуск метки времени останавливает атаки повтора?
Метка времени является частью подписываемого содержимого, поэтому её нельзя изменить, не сломав подпись. Получатель сравнивает её со своими часами и отклоняет всё старше допуска (обычно несколько минут). Это ограничивает, как долго можно воспроизводить захваченный валидный запрос; кэш nonce закрывает повторы внутри этого окна.
Почему требуется сравнение за константное время?
Обычная проверка на равенство возвращается рано на первом несовпадающем байте, и атакующий, способный замерить время ответа, может восстановить корректную подпись побайтно. Сравнение за константное время всегда просматривает полную длину перед возвратом, устраняя побочный канал по времени. Используйте функцию безопасного по времени сравнения платформы.
Как безопасно ротировать секрет подписи webhook?
Используйте перекрывающиеся ключи. Выдайте новый секрет, пока старый остаётся валидным; во время перекрытия отправитель подписывает обоими или получатель принимает любой, так что ни один легитимный webhook не теряется. Как только все производители подтверждены на новом секрете, выведите старый. Префикс схемы на подписи делает переход наблюдаемым.
Разве HTTPS недостаточно, чтобы доверять webhook?
Нет. TLS доказывает, что транспорт зашифрован и клиент достиг вашего сервера, но не то, что запрос пришёл с платформы сообщений — POST на публичный URL может сделать кто угодно. Только подпись HMAC на общем секрете аутентифицирует отправителя и доказывает, что полезная нагрузка не изменена. Список разрешённых IP — слабая замена, поскольку egress-адреса меняются.
Технические характеристики источника
На этой странице представлены опубликованные стандарты, на которых они основаны. (Открывается в новой вкладке)