Что подпись НЕ гарантирует
Верная подпись доказывает две вещи: тело пришло от Oblodai и не изменено в пути. Она не гарантирует:- что это уведомление свежее, а не перехваченное и проигранное заново позже (replay);
- что описанное в теле состояние всё ещё актуально к моменту, когда вы его обрабатываете;
- что вы не обрабатываете это событие повторно (доставка at-least-once).
1. Replay-защита по timestamp
В каждой доставке есть заголовокX-Webhook-Timestamp — момент отправки в unix-секундах, и он входит
в подписанную строку (hex(HMAC-SHA256(secret, "{timestamp}." + сырое_тело))). Значит, timestamp
нельзя подменить, не сломав подпись.
Используйте это: отклоняйте вебхуки со слишком старым timestamp. Тогда даже валидно подписанное, но
перехваченное ранее уведомление нельзя будет проиграть спустя время.
Про окно и ретраи.
X-Webhook-Timestamp обновляется на каждой попытке доставки — при ретрае
Oblodai переподписывает тело со свежей меткой времени. Поэтому окно 300 секунд не отбраковывает
легитимные повторы (даже если сам ретрай случился через час): каждая доставка «свежая» на момент
отправки. 300 c — разумная отправная точка; сужать окно ради «защиты от ретраев» не нужно.2. Дедупликация — защита от повторной обработки
Доставка at-least-once: одно событие может прийти несколько раз (в том числе после/v1/payment/resend или ретраев). Дедуплицируйте по паре
uuid + status и обрабатывайте повтор как no-op.
status/is_final, а не на очерёдность.
3. Перепроверка статуса перед выдачей ценного
Вебхук — сигнал «сходи и проверь», а для дорогих действий (отгрузка товара, крупное зачисление, разблокировка услуги) — не единственный источник правды. Перед необратимой выдачей перепроверьте состояние авторитетным запросом:4. Приёмник: острые углы
- Только HTTPS, публичный адрес. При регистрации URL проходит SSRF-проверку (приватные/локальные
адреса запрещены). →
/v1/webhooks - Сырое тело до парсинга. Подпись считается по байтам; фреймворк, который пересериализует JSON,
сломает проверку (
express.json()на роуте вебхука — частая ошибка). - Секрет вебхука — это секрет. Храните его как ключ API. Помните: повторный вызов
/v1/webhooksменяет URL и выдаёт новый секрет — старый перестанет подходить. - Пробные тела не подписаны (
is_test: true). Ваш код должен их отличать и не проверять подписью — но и не выполнять по ним реальную выдачу. → Отладка вебхуков 2xxтолько после успешной обработки. Иначе Oblodai повторит доставку (это фича, не баг).- Отвечайте быстро, обрабатывайте асинхронно. Тяжёлую работу выносите в очередь, а вебхуку
отвечайте
2xxсразу после того, как надёжно приняли событие — так вы не упрётесь в таймауты доставки и ретраи.
Мини-чек-лист безопасности вебхуков
- Подпись проверяется по сырому телу правильным алгоритмом (timestamp +
.+ тело). - Секрет — из
/v1/webhooks, не ключ API. - Проверяется свежесть по
X-Webhook-Timestamp(replay-защита). - Дедуп по
uuid+statusв надёжном хранилище (не в памяти). - Для дорогих операций — перепроверка статуса через
*/info. - Пробные тела (
is_test) отсекаются от реальной выдачи. 2xxвозвращается только после успешной обработки.- Endpoint только HTTPS, тяжёлая работа — асинхронно.