Подписи веб-хука Проверка содержимого и блокировка поддельных запросов
Узнайте, как работают подписи HMAC-SHA256 вебхуков, как реализовать проверку в постоянное время на Python, Node.js и PHP, и распространённые ошибки, которые приводят к тихим сбоям в производстве.
Любой может отправить POST-запрос на ваш endpoint вебхука. Без проверки подписи ваш сервер не сможет определить, действительно ли запрос пришёл от Stripe, GitHub или вашей собственной инфраструктуры — или от атакующего, который пересылает легитимное событие.
Проверка подписи вебхука решает эту проблему. Это то, как платежные обработчики, платформы контроля версий и системы электронной коммерции позволяют вам подтвердить подлинность перед тем, как действовать на входящие данные. В этой статье описано, как работают подписи HMAC-SHA256, шаг за шагом проверка и незаметные ошибки, которые приводят к сбоям в производственной среде.
Почему неавторизованные вебхуки представляют собой риск безопасности
Эндпоинт вебхука — это просто HTTP-обработчик, открытый для интернета. Без проверки любой запрос, попадающий в него, будет обработан. Две атаки особенно выделяются:
- Подделка — атакующий создаёт фальшивый пакет данных, который выглядит как легитимное событие (успешная оплата, продление подписки) и запускает действия на вашей стороне без реальной транзакции.
- Атака пересылки — легитимный запрос фиксируется в процессе передачи и повторно отправляется позже. Если ваш эндпоинт является идемпотентным, но не защищён, то то же событие будет запускаться несколько раз.
Обе атаки предотвращаются с помощью общего секрета и криптографической подписи. Отправитель подписывает пакет данных; вы проверяете подпись перед обработкой чего-либо.
Как работают подписи HMAC-SHA256
HMAC (хэш-основанная кодировка подлинности сообщения) принимает два входа: секретный ключ и сообщение и определите . Он проходит их через хэш-функцию — SHA-256 в большинстве реализаций вебхуков — и выдаёт фиксированную длину подписи.Ключевое свойство: один и тот же секрет и сообщение всегда дают одинаковую подпись, а изменение даже одного байта в сообщении приводит к совершенно другому результату. Любой, у кого нет секретного ключа, не может создать действительную подпись, даже если он видит полное содержимое пакета.
На практике процесс выглядит так:
Вы регистрируете свой вебхук у сервиса (например, Stripe). Сервис предоставляет вам секретный ключ
- Когда сервис отправляет событие, он вычисляет секрет подписи.
- и включает результат в заголовок запроса.
HMAC-SHA256(secret, payload)Ваш сервер получает запрос, вычисляет тот же HMAC с использованием хранящегося секрета и исходного тела запроса, затем сравнивает две подписи. - Если они совпадают, запрос подлинен. Если нет — отклоняйте его.
- Паттерн проверки, пошагово
Вот реализация на Python, которая соответствует ожиданиям всех основных сервисов:
Здесь два момента важны, кроме самого вызова HMAC. Во-первых:
import hmac
import hashlib
def verify_signature(payload: bytes, secret: str, received_sig: str) -> bool:
expected = hmac.new(
key=secret.encode("utf-8"),
msg=payload,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_sig)
не дешифрованная строка — вы должны передавать payload является bytesисходное тело запроса точно так, как оно получено из сети. Во-вторых: сравнение использует . Это намеренно. hmac.compare_digest вместо ==Почему сравнение за постоянным временем не является опциональным
Сравнение строк в большинстве языков короткое: оно возвращает
сразу, как только обнаруживает несоответствие. Атакующий, способный измерять время ответа, может использовать это — отправляя тысячи запросов с разными подписями и используя различия в времени для угадывания правильного значения по одному символу. Это — атака по времени false в Python, в PHP и.
hmac.compare_digest в Node.js все требуют одинаковое время независимо от того, где строки отличаются. Используйте их каждый раз при сравнении подписи — без исключений. hash_equals Форматы заголовков в реальности: Stripe, GitHub, Shopify crypto.timingSafeEqual Каждый сервис имеет немного отличающееся имя заголовка и формат подписи. Вот то, что нужно парсить для трёх наиболее распространённых интеграций.
Stripe отправляет заголовок
с таким форматом:
Stripe
поле — это Unix-время отправки события. Значение — это HMAC-SHA256 от Stripe-Signature . При ручной реализации:
Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a05bd539e6d5b9d2a2d2fbe
The t Извлеките заголовок для извлечения v1 Вычислите timestamp + "." + raw_bodyСравните с
- используя постоянное время сравнения.
tиv1. - Отклоните, если
HMAC-SHA256(secret, t + "." + raw_body). - отличается более чем на 300 секунд (5 минут) от текущего времени.
v1GitHub использует заголовок - Удалите префикс
tи сравните остаток с HMAC вашего исходного тела. GitHub не встраивает временные метки в подписанное содержимое, поэтому реализуйте дедупликацию событий отдельно, если защита от пересылки важна для вашего случая использования.
GitHub
Shopify использует X-Hub-Signature-256 :
X-Hub-Signature-256: sha256=6ffbb59b2300aae63f272406069a9788598b770f698a48021f99b32f8de06bb3
с дигестом, закодированным в Base64 вместо шестнадцатеричного: sha256= Декодируйте как ваш вычисленный HMAC, так и значение заголовка из Base64 в байты, затем сравните с помощью функции постоянного времени. Прямое сравнение строк в Base64 может привести к незаметным ошибкам нормализации кодирования.
Shopify
Наиболее распространённая ошибка: исходное тело против разобранных JSON X-Shopify-Hmac-Sha256 Здесь чаще всего происходят сбои при проверке подписи в производственной среде. Когда ваша фреймворк автоматически разбирает тело запроса в словарь или объект до запуска обработчика, исходные байты исчезают. HMAC был рассчитан на этих первоначальных байтах — а не на том, что создаёт ваша библиотека JSON при повторной сериализации структуры.
X-Shopify-Hmac-Sha256: b6LPiZidmXnJQf0Ff/p7MZQPIBPN9TqAqLMgAfn2YLQ=
Даже если данные семантически идентичны,
(без пробела после двоеточия) дают разные значения HMAC. Различия в порядке полей, нормализации Unicode и точности вещественных чисел могут незаметно нарушить проверку без каких-либо очевидных ошибок.
Решение:
заблокируйте исходное тело запроса до начала разбора и передавайте эти байты в функцию проверки. В Express.js, зарегистрируйте маршрут с помощью {"amount": 100} и {"amount":100} миддлвара вместо
(байты). В Laravel, используйте . Паттерн одинаков: получайте исходные байты напрямую из запроса, никогда не пересериализуйте разобранный объект. express.raw() Защита от пересылки с помощью временных меток express.json():
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body; // Buffer — not a parsed object
const sig = req.headers['stripe-signature'];
// Verify rawBody against sig before JSON.parse()
});
В Django используйте request.body Действительная подпись подтверждает, что содержимое не было изменено в процессе передачи. Она не предотвращает атакующего, который захватывает легитимный запрос и отправляет его позже — подпись всё равно будет корректной, потому что ничего не изменилось. $request->getContent()Stripe решает эту проблему, включая Unix-время в подписанное содержимое и рекомендует 5-минутный диапазон допуска. После извлечения
из заголовка
отклоняйте запросы, где временная метка находится за пределами этого диапазона:
Для сервисов, таких как GitHub и Shopify, которые не включают временные метки в схему подписи, реализуйте собственную защиту от пересылки, храня обработанные идентификаторы событий (доступные в содержимом) и отклоняя любые идентификаторы, которые уже обрабатывались. Хорошим решением является кратковременный кэш или набор в Redis с TTL, соответствующий вашему временному окну обработки. t Проверка подписи без написания кода Stripe-Signature При отладке интеграции вебхука — или при проверке полученного пакета —
import time
def is_within_tolerance(timestamp: int, tolerance_seconds: int = 300) -> bool:
return abs(time.time() - timestamp) <= tolerance_seconds
# In your handler:
if not is_within_tolerance(stripe_timestamp):
return HttpResponse(status=403) # Reject stale request
позволяет вставить пакет данных, секрет и полученный хэш для мгновенной проверки подлинности без запуска локальной среды.
Для генерации HMAC-подписей для тестирования вашего эндпоинта с созданными пакетами данных, используйте
. Он генерирует выводы в шестнадцатеричной и Base64 форме для SHA-256, SHA-512 и нескольких других алгоритмов хэширования — полезно для создания тестовых случаев, которые охватывают форматы, используемые каждым сервисом. Валидатор подписи веб-перехватчика HMAC-SHA256 с общим секретом — стандартная схема подписи в Stripe, GitHub, Shopify и большинстве других сервисов.
Всегда используйте сравнение за постоянным временем ( Генератор HMAC) — а не
Скорое руководство
- Передавайте исходные байты тела запроса в функцию HMAC, никогда не передавайте пересериализованный JSON-объект.
- Проверяйте временные метки у сервисов, которые их включают; диапазон допуска Stripe составляет 5 минут.
hmac.compare_digest,hash_equals,crypto.timingSafeEqualДедупликация по идентификатору события для сервисов без защиты от пересылки на основе временных меток.==. - Имя заголовка и формат кодирования (шестнадцатеричный vs Base64) различаются у сервисов — всегда проверяйте документацию по интеграции.
- Проверьте время отметки для сервисов, которые включают его; окно допуска Stripe составляет 5 минут.
- Удалите дубликаты по идентификатору события для сервисов без защиты от повторного воспроизведения по времени отметки.
- Название заголовка и кодировка (шестнадцатеричная система или Base64) различаются в зависимости от сервиса — всегда проверяйте документацию по интеграции.
Установите наши расширения
Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска
恵 Табло результатов прибыло!
Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!
Подписаться на новости
все Новые поступления
всеОбновлять: Наш последний инструмент был добавлен 16 Июня 2026
