На публичный endpoint поступают вебхуки партнёра. Как проверить, что запрос действительно сформирован партнёром, а не отправлен злоумышленником на известный URL?
Запрос нужно проверять по криптографической подписи, вычисленной партнёром для тела запроса и проверенной с использованием заранее доверенного секрета или публичного ключа. Один только известный URL, HTTPS или набор полей запроса не доказывает происхождение сообщения.
При общем секрете применяется HMAC: партнёр и получатель знают один секрет, а получатель сравнивает вычисленную подпись с подписью в запросе. При асимметричной схеме партнёр подписывает запрос закрытым ключом, а получатель проверяет подпись публичным ключом.
Публичные HTTP-endpoint получили широкое применение для интеграций, когда одна система должна уведомлять другую о событиях без постоянного опроса. Исходная проблема состояла в том, что сетевой адрес отправителя и сам факт доставки запроса не давали надёжного доказательства его подлинности.
Секретные токены в URL и простая проверка заголовка частично затрудняли случайные обращения, но не обеспечивали криптографическую связь между отправителем и содержимым сообщения. Подпись решает именно эту задачу: изменение сообщения или отсутствие ключа должно сделать проверку неуспешной.
Злоумышленник может узнать адрес endpoint из документации, журналов, клиентского кода или утечки конфигурации. Если обработчик доверяет любому запросу с правильной структурой, атакующий сможет подделать событие: например, инициировать возврат средств, изменить статус заказа или создать внутреннее уведомление.
HTTPS защищает канал между клиентом и сервером, но обычно удостоверяет сервер перед отправителем. Оно не доказывает приложению, что конкретный запрос создал именно партнёр, если любой клиент может подключиться к endpoint.
Неверная проверка также возникает, когда подпись вычисляется не над исходным телом, а над уже разобранным и повторно сериализованным объектом. Различия в пробелах, порядке полей, кодировке или представлении чисел могут привести к ошибкам проверки либо создать неоднозначность для разных компонентов.
Партнёр должен подписывать однозначно определённое представление сообщения, обычно исходные байты тела запроса, а получатель обязан выполнить проверку до бизнес-обработки. Сначала проверяются формат подписи и допустимые параметры алгоритма, затем подпись вычисляется с доверенным ключом и сравнивается с полученной криптографически безопасным способом.
У схемы на общем секрете есть простая модель доверия: тот, кто знает секрет, способен создавать допустимые сообщения. Поэтому отдельный секрет нужен для каждого партнёра или доверенной зоны, а хранить его следует в защищённом хранилище секретов с ограниченным доступом.
Асимметричная подпись удобнее при большом числе получателей или организационной границе: партнёру не требуется передавать получателю секрет, достаточный для создания новых подписей. Однако нужно надёжно управлять публичными ключами, их идентификаторами, сроком действия и отзывом.
Подпись подтверждает целостность и владение ключом, но сама по себе не предотвращает повторную отправку ранее корректного сообщения. Для защиты от повторов обычно проверяют временную метку или уникальный идентификатор события, ограничивают допустимое окно времени и сохраняют обработанные идентификаторы с подходящей политикой хранения.
Нужно заранее определить поведение при ошибке: неподписанный, просроченный или неверно подписанный запрос отклоняется без выполнения побочных действий. Ответы не должны раскрывать, какой именно этап проверки не пройден, а повторные доставки корректного события должны обрабатываться идемпотентно.
Платёжный провайдер отправлял магазину уведомление об успешной оплате. Команда рассматривала три варианта: разрешить запросы только с опубликованных IP-адресов, передавать секрет в URL или проверять подпись тела запроса.
Фильтрация по IP уменьшала поверхность атаки, но зависела от актуальности списков и не доказывала целостность содержимого. Секрет в URL был прост в реализации, однако мог попасть в журналы, историю прокси или системы мониторинга. Подпись тела требовала аккуратной работы с исходными байтами и ротацией ключей, зато связывала сообщение с ключом провайдера и не раскрывала секрет в адресе.
Выбрали подпись с идентификатором ключа, временной меткой и уникальным идентификатором события. Запрос проверялся до изменения состояния платежа, а уже обработанные события сохранялись для идемпотентной повторной доставки. Это позволило сохранить надёжность интеграции при сетевых повторах и одновременно исключить подделку уведомлений без ключа.
1. Достаточно ли проверять подпись только после разбора JSON?
Нет, это может быть небезопасно или нестабильно. Подпись должна проверяться над тем представлением, которое подписал отправитель, обычно над исходными байтами тела; повторная сериализация может изменить пробелы, порядок или кодировку. Если инфраструктура неизбежно преобразует тело, формат канонизации должен быть явно согласован всеми сторонами.
2. Защищает ли подпись от повторной отправки корректного запроса?
Нет. Перехваченный или ранее полученный корректно подписанный запрос сохраняет действительную подпись. Для защиты от повторов нужны временные ограничения, уникальный идентификатор события и хранилище уже обработанных идентификаторов; при этом окно времени и срок хранения выбирают с учётом задержек и повторных доставок.
3. Почему недостаточно сравнить полученную подпись обычным сравнением строк?
Обычное посимвольное сравнение может завершаться на первой несовпавшей позиции, а различия во времени обработки потенциально раскрывают информацию о совпадающем префиксе. Для проверки криптографических значений применяют сравнение с постоянным временем, корректно декодируют формат подписи и однозначно проверяют длину и алгоритм. Это не заменяет управление ключами, но устраняет отдельный канал утечки при самой проверке.