Уроки, которые оставил инцидент React RCE: почему сейчас нужны HMAC‑подписи, ротация ключей и нулевое доверие

Что показала уязвимость RCE: «доверять данным – значит потерять контроль»



Недавняя уязвимость RCE (CVE-2025-55182) в React Server Components/Next.js не просто «React взломан». Она демонстрирует более фундаментальный принцип.

«Если вы когда‑то доверяете данным, пришедшим от клиента, вы обязательно потеряете контроль»

Суть уязвимости в том, что сервер использовал данные Flight‑протокола (метаданные), отправленные клиентом, без надлежащей проверки, напрямую в загрузку модулей и доступ к объектам.

  • «Эти данные React сам безопасно обработает»
  • «Поскольку это не наш собственный API, всё ок»

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

Теперь вопрос меняется.

  • «Почему React взломан?»
  • «Какие данные мы сейчас принимаем без подписи и проверки?»

Ответы на этот вопрос естественно приводят к концепциям HMAC‑подписей и нулевого доверия.


Почему важны HMAC‑подписи: «Эти данные действительно созданы нашим сервером?»

В реальных системах «доверие между серверами» играет большую роль.

  • Frontend ↔ Backend
  • Микросервисы A ↔ B
  • Backend ↔ Backend (асинхронные задачи, очередь, Webhook, внутренний API и т.д.)

Данные, передаваемые между этими компонентами, часто считаются безопасными по умолчанию.

«Этот URL не будет раскрыт наружу, поэтому только внутренние системы его вызовут»

«Формат токена известен только нашему сервису, значит безопасен»

Но злоумышленник всегда пытается нарушить эти предположения.

Здесь HMAC (например, HMAC‑SHA256) отвечает на фундаментальный вопрос.

«Создал этот запрос/сообщение кто‑то, кто знает наш секретный ключ?»

Если развернуть: только владелец ключа может создать действительную подпись, а сервер проверяет payload + signature на корректность и отсутствие подмены.

Мир без HMAC‑подписей

Как бы выглядел REST/Webhook/внутренний API без HMAC‑подписей?

  • Если злоумышленник знает структуру URL и параметры, он может
  • Создать произвольный запрос и заставить сервер принять его как «естественный внутренний запрос».

Например:

POST /internal/run-action
Content-Type: application/json

{
  "action": "promoteUser",
  "userId": 123
}

Хотя это внутренний эндпоинт, если * Пределы сети нарушены, * Есть уязвимость в внутреннем прокси, * CI/CD, логи, примеры кода, документация доступны внешнему миру,

злоумышленник может «подменить» внутренний сервис.

Если бы HMAC‑подпись была, ситуация изменилась бы.

  • Подпись прикрепляется к телу запроса.
  • Сервер проверяет подпись для каждого запроса.

«Понимаете URL и параметры, но без ключа вы не создадите валидный запрос»


Пример: защита внутренних действий с помощью HMAC‑подписей



Простой пример (псевдокод в стиле TypeScript/Node.js).

1. Предположим, сервер хранит общий секретный ключ

const HMAC_SECRET = process.env.HMAC_SECRET!; // из .env или Secret Manager

2. Клиент (или внутренний сервис) формирует запрос

import crypto from 'crypto';

function signPayload(payload: object): string {
  const json = JSON.stringify(payload);
  return crypto
    .createHmac('sha256', HMAC_SECRET)
    .update(json)
    .digest('hex');
}

const payload = {
  action: 'promoteUser',
  userId: 123,
};

const signature = signPayload(payload);

// Отправка
fetch('/internal/run-action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Signature': signature,
  },
  body: JSON.stringify(payload),
});

3. Сервер проверяет подпись

function verifySignature(payload: any, signature: string): boolean {
  const json = JSON.stringify(payload);
  const expected = crypto
    .createHmac('sha256', HMAC_SECRET)
    .update(json)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

app.post('/internal/run-action', (req, res) => {
  const signature = req.headers['x-signature'];

  if (!signature || !verifySignature(req.body, String(signature))) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Достигнув сюда значит:
  // 1) payload не был изменён
  // 2) запрос сформирован тем, кто знает HMAC_SECRET
  handleInternalAction(req.body);
});

Эта схема – базовый паттерн доказательства того, что «данные действительно пришли от нас».


Если в команде несколько администраторов/разработчиков: ротация ключей – «не опция, а необходимость»

Еще одна реальная проблема HMAC‑ключей – это люди.

  • Операторы, SRE, backend‑разработчики, подрядчики, стажёры…
  • Не все имеют доступ к .env, но ключ может попасть в GitOps, CI/CD, документацию, Slack, Notion и т.д.

Ключевой момент:

«Ключ может утечь» – это предположение, на котором строится система.

То есть «мы считаем, что утечка невозможна» – это ошибка.

Поэтому ротация ключей становится обязательной.

Зачем нужна ротация

  1. Люди меняются * Уход, смена команды, завершение подрядчика * Тот, кто видел ключ, больше не должен иметь к нему доступ
  2. Невозможно полностью отследить утечку * Slack‑DM, локальные заметки, скриншоты… * «Мы никогда не утекли ключ» – практически невозможно
  3. Нужно иметь возможность восстановиться после инцидента * Если ключ утек до определённого момента, можно объявить, что «с этого момента принимаются только запросы, подписанные новым ключом»

Практический паттерн ротации

Для организаций с несколькими администраторами обычно используют следующий подход.

  1. Управление версиями ключей * HMAC_SECRET_V1, HMAC_SECRET_V2 и т.д. * В заголовке или payload передаётся kid (идентификатор ключа)
  2. Сервер хранит несколько ключей и проверяет запросы по версии * Например, сначала проверяем V1, если не подходит – V2 * После определённого периода удаляем V1, оставляем только V2
  3. Документируем процесс ротации * «Создать новый ключ → развернуть → переключить генератор и проверяющий → удалить старый» * Список действий + runbook, чтобы любой новый сотрудник мог выполнить
  4. Разделение прав * Человек, создающий ключ, и человек, меняющий код, – разные роли

Когда HMAC‑ключ становится «одним значением», утечка приводит к полной краху системы. Ротация ограничивает риск во времени.


Нулевое доверие: «Доверие – это не структура, а проверка каждого запроса»

Наконец, из инцидента React RCE следует напомнить базовый принцип безопасности.

Основой безопасности является нулевое доверие (Zero Trust).

Ключевые идеи нулевого доверия:

  • «Внутреннее» не значит «безопасное»
  • «Только фронтенд» не значит «безопасно»
  • «Мы создали фреймворк» не значит «защищено»

Вместо этого задавайте вопросы:

  • «Мы предположили, что вход может быть злонамеренным?»
  • «Есть ли доказательство, что запрос пришёл от ожидаемого субъекта?»
  • «Как убедиться, что данные не были изменены?»
  • «Если ключ утечет, как быстро мы сможем ограничить ущерб?»

React RCE демонстрирует, как «доверие» к Flight‑данным привело к тому, что клиентские данные напрямую влияли на загрузку серверных модулей.

С точки зрения нулевого доверия, это должно было выглядеть так:

«Серийные данные, которые клиент может изменить, влияют на путь выполнения кода – это всегда сигнал опасности»

И первая защита – это:

  • HMAC‑подпись (целостность и происхождение)
  • Ротация ключей (ограничение ущерба при утечке)

Итоги: «Не доверяй, а проверяй»

Три ключевых послания:

  1. Не доверяйте данным по умолчанию * Даже «внутренние» данные, прошедшие через сеть, могут стать поверхностью атаки.
  2. HMAC‑подпись – базовый инструмент проверки происхождения * REST‑API, Webhook, внутренний RPC, асинхронные сообщения – все места, где стоит использовать HMAC. * При наличии нескольких администраторов обязательно включайте ротацию ключей.
  3. Нулевое доверие – фундамент * Доверие не даётся автоматически, а создаётся через проверку каждого запроса.

Инцидент React RCE напоминает, что границы между фронтендом и сервером размываются, и нам нужно постоянно пересматривать наши предположения о «внутреннем».

«Не доверяй, а проверяй. Проверку делай в коде, процессах и политике управления ключами».

Первый шаг – HMAC‑подписи и ротация ключей, над которым стоит строить философию нулевого доверия.

image