Подписи веб-хука Проверка содержимого и блокировка поддельных запросов

Обновлено

Узнайте, как работают подписи HMAC-SHA256 вебхуков, как реализовать проверку в постоянное время на Python, Node.js и PHP, и распространённые ошибки, которые приводят к тихим сбоям в производстве.

Подписи веб-хука: проверка содержимого и блокировка поддельных запросов 1
Реклама · УДАЛИТЬ?

Любой может отправить POST-запрос на ваш endpoint вебхука. Без проверки подписи ваш сервер не сможет определить, действительно ли запрос пришёл от Stripe, GitHub или вашей собственной инфраструктуры — или от атакующего, который пересылает легитимное событие.

Проверка подписи вебхука решает эту проблему. Это то, как платежные обработчики, платформы контроля версий и системы электронной коммерции позволяют вам подтвердить подлинность перед тем, как действовать на входящие данные. В этой статье описано, как работают подписи HMAC-SHA256, шаг за шагом проверка и незаметные ошибки, которые приводят к сбоям в производственной среде.

Почему неавторизованные вебхуки представляют собой риск безопасности

Эндпоинт вебхука — это просто HTTP-обработчик, открытый для интернета. Без проверки любой запрос, попадающий в него, будет обработан. Две атаки особенно выделяются:

  • Подделка — атакующий создаёт фальшивый пакет данных, который выглядит как легитимное событие (успешная оплата, продление подписки) и запускает действия на вашей стороне без реальной транзакции.
  • Атака пересылки — легитимный запрос фиксируется в процессе передачи и повторно отправляется позже. Если ваш эндпоинт является идемпотентным, но не защищён, то то же событие будет запускаться несколько раз.

Обе атаки предотвращаются с помощью общего секрета и криптографической подписи. Отправитель подписывает пакет данных; вы проверяете подпись перед обработкой чего-либо.

Как работают подписи HMAC-SHA256

HMAC (хэш-основанная кодировка подлинности сообщения) принимает два входа: секретный ключ и сообщение и определите . Он проходит их через хэш-функцию — SHA-256 в большинстве реализаций вебхуков — и выдаёт фиксированную длину подписи.Ключевое свойство: один и тот же секрет и сообщение всегда дают одинаковую подпись, а изменение даже одного байта в сообщении приводит к совершенно другому результату. Любой, у кого нет секретного ключа, не может создать действительную подпись, даже если он видит полное содержимое пакета.

На практике процесс выглядит так:

Вы регистрируете свой вебхук у сервиса (например, Stripe). Сервис предоставляет вам секретный ключ

  1. Когда сервис отправляет событие, он вычисляет секрет подписи.
  2. и включает результат в заголовок запроса. HMAC-SHA256(secret, payload) Ваш сервер получает запрос, вычисляет тот же HMAC с использованием хранящегося секрета и исходного тела запроса, затем сравнивает две подписи.
  3. Если они совпадают, запрос подлинен. Если нет — отклоняйте его.
  4. Паттерн проверки, пошагово

Вот реализация на 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Сравните с

  1. используя постоянное время сравнения. t и v1.
  2. Отклоните, если HMAC-SHA256(secret, t + "." + raw_body).
  3. отличается более чем на 300 секунд (5 минут) от текущего времени. v1 GitHub использует заголовок
  4. Удалите префикс 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) различаются в зависимости от сервиса — всегда проверяйте документацию по интеграции.
Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?