HMAC — как вебхуки узнают, что вы не лжете
Любой сервер может отправить запрос POST на ваш endpoint веб-хука. Подписи HMAC позволяют легитимным отправителям подтвердить, что они создали содержимое — и позволяют вам проверить это.
Каждый раз, когда Stripe заряжает карту, запускается вебхук. Каждый раз, когда GitHub объединяет запрос, запускается вебхук. Эти платформы отправляют POST-запросы на URL, который вы им предоставили — но вот неприятная правда: любой другой может отправить POST-запрос на тот же URL.
Итак, как ваш сервер знает, что запрос действительно пришёл от Stripe, а не от атакующего, который угадал ваш URL-конечную точку? Ответ — HMAC — и как только вы понимаете это, вы поймёте, почему это стандартный подход во всех серьёзных платформах API.
Проблема: Любой может отправить POST-запрос на вашу конечную точку
Вебхук-конечные точки — это просто URL. Они публично доступны (они должны быть, чтобы отправитель мог добраться до них), и они принимают POST-запросы. Нет ничего, что бы мешало злоумышленнику создать фальшивый пакет и отправить его на вашу конечную точку.
Представьте, что ваш обработчик вебхука делает это при получении события:
if event["type"] == "payment.completed":
fulfill_order(event["data"]["order_id"])
Атакующий, который знает ваш URL-конечную точку, может отправить фальшивое payment.completed событие с любым идентификатором заказа, который ему нравится. Без проверки ваш сервер с радостью выполнял заказы, которые никогда не платились.
Вам нужно средство для проверки того, что пакет был создан тем, кто владеет секретом, который вы оба обмениваете — без передачи этого секрета в запросе.
Что такое HMAC?
HMAC означает Код аутентификации сообщения на основе хеша. Это конструкция, которая объединяет криптографическую хеш-функцию (обычно SHA-256) с секретным ключом для создания подписи. Подпись доказывает две вещи:
- Подлинность — сообщение было создано тем, кто владеет секретным ключом
- Целостность — сообщение не было изменено в процессе передачи
Ключевое свойство HMAC: вы не можете создать действительную подпись без знания секрета. И вы не можете восстановить секрет из подписи. Это односторонняя проверка.
HMAC против простого хеша
Простой хеш (например, SHA-256) пакета решает проблему целостности, но не решает проблему подлинности. Атакующий, который перехватывает действительный пакет, может пересчитать хеш изменённого пакета. HMAC включает ваш секретный ключ на каждом этапе хеширования, поэтому без ключа вы не можете создать совпадающую подпись, даже если вы знаете используемый хеш-алгоритм.
Как работает HMAC вебхука
Процесс состоит из трёх шагов: обмен ключом, подписание и проверка.
Шаг 1: Обмен ключом (происходит один раз)
Когда вы настраиваете вебхук с платформой, такой как Stripe или GitHub, они генерируют секрет вебхука и показывают его вам один раз. Вы храните его на сервере (никогда не в клиентском коде или в публичных репозиториях). Это всё — секрет никогда не передаётся по сети снова.
Шаг 2: Отправитель подписывает пакет
Перед отправкой вебхука платформа вычисляет подпись HMAC над исходным телом запроса с использованием вашего общего секрета:
signature = HMAC-SHA256(secret_key, request_body)
Подпись затем прикрепляется к запросу, обычно в заголовке вроде X-Hub-Signature-256 (GitHub) или Stripe-Signature (Stripe). Исходный пакет передаётся в теле без изменений.
Шаг 3: Вы проверяете при получении
Когда ваш сервер получает вебхук, вы пересчитываете тот же HMAC с использованием исходного тела и вашего хранящегося секрета, затем сравниваете результат с подписью в заголовке. Если они совпадают, пакет является подлинным и не изменённым. Если не совпадают, отклоняйте его.
expected = HMAC-SHA256(your_secret, raw_body)
if not constant_time_equal(expected, header_signature):
return 401
Уведомление постоянное время сравнения — мы вернёмся к тому, почему это важно.
Реальные примеры
Stripe
поле — это Unix-время отправки события. Значение — это HMAC-SHA256 от Stripe-Signature заголовок, содержащий временные метки и одну или несколько подписей:
Stripe-Signature: t=1679000000,v1=abc123...,v0=oldformat...
Подпись вычисляется над timestamp.payload (сопоставленной с точкой). Включение временной метки позволяет Stripe защищаться от атак пересылки — если атакующий перехватывает действительный подписаный запрос и отправляет его позже, вы можете отклонить его, потому что временная метка слишком старая.
GitHub
GitHub отправляет X-Hub-Signature-256 заголовок в формате sha256=<hex_digest>. Подпись — это HMAC-SHA256 исходного тела с использованием секрета вебхука, который вы настроили в настройках репозитория.
Shopify
Shopify использует заголовок X-Shopify-Hmac-Sha256 с Base64-кодированной подписью HMAC-SHA256 — тот же концепт, но другой формат кодирования.
Проверка в коде
Вот как выглядит проверка в трёх распространённых языках. Паттерн одинаков — только вызовы библиотек отличаются.
Питон
import hmac
import hashlib
def verify_webhook(secret: str, payload: bytes, signature_header: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
Node.js
const crypto = require('crypto');
function verifyWebhook(secret, rawBody, signatureHeader) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = signatureHeader.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
);
}
PHP
function verifyWebhook(string $secret, string $rawBody, string $signatureHeader): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
Ошибки, которые нарушают безопасность
1. Использование == вместо сравнения постоянного времени
Стандартное сравнение строк (==, ===) коротко останавливается как только обнаруживает несоответствие. Это создаёт канал времени: атакующий может измерить, сколько времени ваш сервер тратит на отклонение различных подписей. Строки, которые имеют более длинный префикс, отклоняются немного дольше. С помощью достаточного количества запросов атакующий может использовать это, чтобы пошагово восстановить действительную подпись.
Всегда используйте сравнение постоянного времени: hmac.compare_digest() в Node.js все требуют одинаковое время независимо от того, где строки отличаются. Используйте их каждый раз при сравнении подписи — без исключений. crypto.timingSafeEqual() в Node.js, hash_equals() в PHP.
2. Парсинг тела перед проверкой
HMAC вычисляется над исходными байтами тела запроса. Если вы сначала парсите JSON, а затем пересоздаёте его, вы можете получить другой последовательность байт (другой порядок ключей, пробелы, кодировка). Всегда сохраняйте исходное тело до того, как ваша фреймворк-обработчик тела касается его, затем проверяйте на основе этого.
3. Не проверка атак пересылки
Действительный подписаный запрос является действительным вечно — если вы не проверяете временные метки. Если платформа включает временные метки в свою схему подписи (Stripe делает это; GitHub не делает), отклоняйте запросы, где временная метка старше нескольких минут. Это предотвращает атакующему перехват и повторную передачу легитимного запроса.
4. Прямое включение секрета в исходный код
Секреты вебхука должны храниться в переменных среды или в менеджере секретов, никогда не коммитаться в систему контроля версий. Утечка секрета означает, что атакующий может подделывать любой пакет вечно — до тех пор, пока вы не замените его.
Что HMAC не защищает
HMAC доказывает, что пакет был подписан тем, кто владеет секретом. Оно нет не защищает от:
- Уязвимость отправителя — если компоненты подписи Stripe были скомпрометированы, фальшивые события всё равно будут иметь действительные подписи
- Атака пересылки — если вы не проверяете временные метки или нон-секреты
- Конфиденциальность — HMAC не шифрует ничего; пакет передаётся в открытом виде (хотя HTTPS решает эту проблему)
Для большинства интеграций вебхуков HMAC над HTTPS покрывает всё, что вам нужно.
Установите наши расширения
Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска
恵 Табло результатов прибыло!
Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!
Подписаться на новости
все Новые поступления
всеОбновлять: Наш последний инструмент Конвертер Excel (XLSX) в CSV 1
