Webrium
Подписаться

Подпись вебхука: как понять, что вызов настоящий

Как правильно проверять подписи вебхуков и почему секретный URL не защищает от атак

Сравнение подписи вебхука: совпадение подтверждает подлинность, несовпадение — подделкуПодпись совпалаПодпись неверна

Зачем подпись, если канал уже закрыт

Вебхук — это HTTP-запрос, который отправляет не клиент, а сервер. В обычном API клиент стучится к серверу, и сертификат предъявляет сервер: он доказывает клиенту, что тот попал на нужный домен, а о самом клиенте канал не сообщает ничего. В вебхуке меняются не правила TLS, а роли: стучится к вам сервер отправителя, а сервером в канале оказывается ваш сервис — значит, сертификат предъявляете вы, и он подтверждает отправителю, что тот попал на нужный домен. Этим закрытый канал и исчерпывается — шифрование и подтверждение домена приёмника; сам этот слой, от сертификата до проверки, разобран отдельно, в материале HTTPS с нуля: сертификат, перенаправление, проверка. Но односторонний TLS о личности отправителя не сообщает ничего — она появляется на уровне канала только при взаимной проверке сертификатов, которой в обычной интеграции с чужим отправителем нет. Доказательство того, кто вызвал, переезжает в содержимое запроса: отправитель кладёт рядом с телом подпись, а вы пересчитываете её своим секретом и сравниваете с присланной.

Подпись вебхука — это не подпись тела как такового. И это не та же задача, что проверка токена, который вы выдали сами: там вы проверяете собственный выпуск собственным ключом, здесь — чужое событие ключом, согласованным с чужой стороной; устройство такого токена разобрано отдельно, в материале JWT: что это на самом деле и где он не подходит. Что именно попадает под подпись, разбираем ниже.

Тупик: секретный адрес вместо подписи

Первое, что пробуют вместо подписи, — секретный адрес приёмника. Идея простая: вместо того чтобы проверять подпись, делают путь к обработчику длинным и непредсказуемым — например, /webhook/3a7b9c2d-e4f5-6789-0123-456789abcdef. Логика такая: если злоумышленник не знает этот путь, он не сможет отправить запрос.

Но секретный адрес не решает ни одну из проблем, которые должна решать подпись. Вот что происходит на самом деле:

  1. Адрес оседает в журналах. Каждый запрос проходит через прокси, балансировщик, инструменты мониторинга. Путь целиком логируется, и если хотя бы один из этих компонентов скомпрометирован, секрет перестаёт быть секретом.

  2. Адрес не защищает от повтора. Если злоумышленник перехватил запрос — например, из журналов прокси — он может отправить его повторно, и ваш сервис примет его как настоящий. Подпись защищает от повтора, потому что включает отметку времени, а секретный адрес — нет.

  3. Адрес не защищает от подмены тела. Если злоумышленник знает адрес, он может отправить запрос с любым телом, и ваш сервис примет его как настоящий. Подпись защищает от подмены, потому что сырые байты тела входят в подписываемую строку, а секретный адрес их не покрывает.

Секретный адрес — это иллюзия безопасности. Он не заменяет подпись, а лишь создаёт ложное чувство защищённости.

Рядом с секретным адресом ставят вторую подпорку — список разрешённых адресов отправителя. Список адресов проверяет маршрут, а не отправителя. Он ломается при смене инфраструктуры у партнёра: меняется адрес или домен — список приходится обновлять. И он же подделывается, если перед приложением стоит прокси, а адрес клиента приложение берёт из заголовка как есть: заголовок ставит тот, кто отправляет запрос, поэтому в список проходит любой, кто впишет туда разрешённый адрес. Подпись не зависит от адреса отправителя.

Что подписывают на самом деле

Подпись считают по строке, которую собрал отправитель. В этой строке могут быть:

  • сырые байты тела запроса,
  • идентификатор события,
  • отметка времени,
  • разделитель между частями.

Порядок частей и разделитель задаёт отправитель. Если отправитель говорит, что подписывает тело и отметку времени через двоеточие, а вы подписываете тело и идентификатор через запятую, подписи не совпадут.

Байты тела берут сырыми, потому что разбор и повторная сериализация меняют строку. Вот где сырые байты теряются чаще всего:

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

  • Запрос переписал посредник перед приёмником. Прокси, шлюз или сеть доставки не меняют смысл события, но принимают запрос на себя и передают дальше уже своей передачей: сжатое тело распаковывается, собирается из частей заново, заголовки длины и кодирования передачи переписываются под посредника. Речь не только о внешней сети доставки — так же ведёт себя обратный прокси, стоящий перед приложением. Например, отправитель подписал сжатые байты, а до вашего обработчика тело дошло уже распакованным — подписывали одни байты, считаете по другим.

  • Тело пришло form-encoded. Если запрос пришёл как application/x-www-form-urlencoded, а подписана сырая форма, а не декодированное поле внутри неё, подпись не сойдётся. Например, если отправитель подписал строку field1=value1&field2=value2, а вы разобрали её в объект и собрали обратно, порядок полей может измениться.

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

Когда проверка не сходится на каждом запросе

Если проверка написана и не сходится на каждом запросе, проблема обычно в кодировке. Вот типичные ошибки:

  1. Секрет не декодирован. Отправитель часто выдаёт секрет в base64 или hex. Если вы используете его как строку символов, а не как байты, подпись не сойдётся. Например, если секрет в base64 — c2VjcmV0, а вы используете его как строку "c2VjcmV0", а не как байты [0x73, 0x65, 0x63, 0x72, 0x65, 0x74], подпись будет неверной.

  2. Подпись не приведена к байтам. Подпись может приходить в hex или base64, в верхнем или нижнем регистре, с префиксом схемы через знак равенства перед значением. Перед сравнением её приводят к байтам: убирают префикс схемы и декодируют — из hex или base64, смотря в каком виде она пришла. Регистр при этом — свойство печатной записи, а не байтов: у hex он не значащий, и выравнивать его, если вообще нужно, приходится до декодирования, а в base64 регистр значащий, и трогать его нельзя. К уже полученным байтам регистр неприменим: в некоторых языках у байтовой строки всё же есть операция приведения к нижнему регистру, и она молча портит подпись.

Отдельно — то, что на несовпадение не влияет вовсе. Сравнение с постоянным временем на тех же байтах даёт тот же ответ, что и обычное: несошедшуюся подпись оно не чинит и к кодировке отношения не имеет. Обычное сравнение строк завершается на первом различии, и по сети эта разница тонет в дрожании: подбор по времени ответа потребовал бы многих замеров при стабильном поведении приёмника. Спорить об этом не нужно — раз проверку всё равно переписывают, константное сравнение стоит одну строку кода и снимает вопрос целиком.

При отладке ни подпись, ни секрет, ни тело события целиком в журналы не пишут. Тело — это чужие персональные данные, которые вы приняли, но не запрашивали.

Повтор запроса: откуда берётся валидный запрос

Валидный запрос для повтора берётся не с провода — HTTPS канал закрывает. Реальные источники:

  • Повторная доставка самим отправителем. Отправитель может повторять доставку на любой ответ, который сам не счёл успешным. Например, если ваш сервис вернул ошибку сервера (5xx), отправитель будет повторять запрос по расписанию.

  • Запись в журналах. Тело и заголовки запроса могут быть записаны в журналах прокси, балансировщика или приложения. Если эти журналы скомпрометированы, злоумышленник может повторить запрос.

  • Зеркалирование трафика. Если вы используете зеркалирование трафика для отладки или мониторинга, копия запроса может быть сохранена и использована для повтора.

  • Скомпрометированный посредник. Если посредник — например, прокси или балансировщик — скомпрометирован, злоумышленник может сохранить и повторить запрос.

  • Тестовый приёмник. Если вы используете тестовый приёмник для отладки интеграции, он может получать те же события, что и основной. Если тестовый приёмник скомпрометирован, злоумышленник может повторить запрос.

Отметка времени защищает от повтора только тогда, когда входит в ту же строку, по которой считается подпись. Переданная отдельно от подписываемых данных, она подменяется, и защита исчезает.

Допуск на расхождение часов двусторонний: отметка времени может оказаться и позади, и впереди часов приёмника. Ширину окна выбирают так, чтобы она покрывала максимальное расхождение часов между отправителем и получателем, плюс время на повторную доставку. Чем шире окно, тем больше повторов внутри него проходит.

Идемпотентность по идентификатору события разобрана отдельно — в материале о проектировании API. Здесь важно другое: порядок доставки она не чинит. Если два разных события об одном объекте приходят в обратном порядке, обработка более старого затирает свежее состояние. Лечится это версией или временем изменения объекта внутри события либо обращением к API отправителя за текущим состоянием.

Как устроен обработчик

Порядок работы обработчика:

  1. Сырые байты тела и предел размера. Предел выбирают так, чтобы он покрывал максимальный размер события у вашего отправителя. Если тело превышает предел, обработчик сразу возвращает ошибку клиента (4xx), не тратя ресурсы на криптографию.

  2. Дешёвая проверка свежести. Отметка времени из заголовка сравнивается с текущим временем. Если расхождение превышает допустимое окно, обработчик возвращает ошибку клиента. Это дешёвая отсечка до криптографии.

  3. Подпись. Подпись считается по сырым байтам тела и другим частям строки, заданным отправителем. Если подпись не совпадает, обработчик возвращает ошибку клиента.

  4. Разбор JSON. Только после проверки подписи тело разбирается в объект. Разбор — это работа над неподтверждёнными данными, поэтому его делают после криптографии.

Код ответа управляет повторами отправителя:

  • Успешный ответ (2xx) означает «принято, не повторять». Его отдают после сохранения события или постановки в очередь, а не после обработки.

  • Ошибка клиента (4xx) говорит «повторять бессмысленно». Например, если подпись не сошлась, отправитель не должен повторять запрос.

  • Ошибка сервера (5xx) говорит «повторите». Отправитель будет повторять запрос по расписанию.

На несошедшуюся подпись ставят код ошибки клиента — успешный ответ на неё прячет атаку и ломает разбор инцидента.

Как жить с секретом

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

Секрет свой на каждую пару «отправитель — приёмник» и на каждый адрес приёмника — где его хранить и как раздавать, разобрано в материале Секреты в CI: пять типовых утечек. Один секрет на все интеграции делает ротацию невозможной без простоя, а компрометацию одной превращает в компрометацию всех.

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

  1. Отправитель в период ротации кладёт в один заголовок несколько подписей. Например, в заголовке с подписью через разделитель стоят два значения: одно посчитано прежним секретом, другое — новым.

  2. Приёмник держит набор действующих ключей. Он перебирает все ключи и ищет любое совпадение. Перебор проходят до конца, без раннего выхода на первом совпадении.

Идентификатор ключа рядом с подписью лишь укорачивает перебор, но не обязателен. Например, заголовок с подписью может нести два значения через разделитель: ключ1=схема=abc123,ключ2=схема=def456.

Позиция редакции

Подпись считают по сырым байтам, собранным в строку ровно по правилу отправителя. В эту строку включают отметку времени — без любого из этих двух условий проверка декоративна. Сравнивают за постоянное время: не потому, что иначе подпись подберут, а потому что это одна строка кода, снимающая вопрос целиком.

Если фреймворк не отдаёт сырое тело, меняют способ чтения запроса, а не подписывают разобранный и заново собранный объект.

Позиция неверна, если отправитель и получатель — ваши собственные сервисы со взаимной проверкой сертификатов. Канал уже аутентифицирует обе стороны, отдельная подпись добавляет обслуживание секрета без новой гарантии, и достаточно идемпотентности по идентификатору события.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Схема стыка JWT: проверенная подпись и проблема отзыва токенаПодпись проверенаОтзыв невозможен
Предыстория

JWT: что это на самом деле и где он не подходит

JWT: зачем он нужен, как работает и где его использование приводит к проблемам с безопасностью и производительностью.

Разделение подходов: проектирование от экранов и от структуры данныхЭкраны интерфейсаСтруктура данных
Практика

REST API: что решить до первого маршрута

Как спроектировать API, чтобы не переписывать его через полгода — от структуры данных до кодов ошибок и версионирования.

Цепочка шагов конвейера CI: чужой код входит, исполняется с ключами, и последний шаг — публикация — штатно выводит секрет за границу доверияграница довериясекрет наружу
Усиление

Секреты в CI: пять типовых утечек

Как защитить секреты в CI, если хранилище и маскировка логов — не панацея: пять реальных уязвимостей и как их закрыть.

Цепочка настройки HTTPS: от привязки домена до выпуска сертификата через проверкуПроверка доменаВыпуск сертификата
Усиление

HTTPS с нуля: сертификат, перенаправление, проверка

Как правильно настроить HTTPS — от выпуска сертификата до проверки безопасности, чтобы избежать ошибок и защитить трафик.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

↑↓ выбрать · Enter открыть · Esc закрыть