Lecciones del incidente RCE de React: ¿por qué ahora las firmas HMAC, la rotación de claves y Zero Trust?

Lo que mostró la vulnerabilidad RCE: "El momento en que confías en los datos es el final"



La reciente vulnerabilidad RCE (CVE-2025-55182) en React Server Components/Next.js no debe reducirse a la simple noticia de que "React está comprometido". El mensaje central que este incidente revela es mucho más fundamental.

"Si alguna vez confías en los datos enviados por el cliente, eventualmente será explotado."

La esencia de esta vulnerabilidad radica en que el servidor utilizó los datos del protocolo Flight (metadatos) enviados por el cliente sin una validación adecuada, y los empleó directamente para la carga de módulos y el acceso a objetos.

  • "React se encargará de que sea seguro."
  • "No es una API que usamos directamente, así que está bien."

Esta confianza implícita acumulada llevó a un punto donde la RCE era posible sin autenticación previa.

Ahora debemos cambiar la pregunta.

  • En lugar de "¿por qué React se vio comprometido?", debemos preguntar
  • "¿Qué datos estamos confiando sin firmar ni validar?"

Al buscar la respuesta a esa pregunta, surgen naturalmente los conceptos de firmas HMAC y Zero Trust.


Por qué son importantes las firmas HMAC: "¿Son realmente estos datos creados por nuestro servidor?"

Los sistemas reales dependen mucho de la "confianza entre servidores".

  • Frontend ↔ Backend
  • Microservicio A ↔ B
  • Backend ↔ Backend (trabajos asíncronos, colas, Webhook, API internas, etc.)

Los datos que circulan entre estos puntos a menudo se asumen de la siguiente manera.

"Esta URL no se expone al exterior, solo los sistemas internos la llamarán."

"Este formato de token es conocido solo por nuestro servicio, por lo que es seguro."

Pero los atacantes siempre intentan romper estas suposiciones.

En este contexto, HMAC (por ejemplo, HMAC-SHA256) existe para responder a una pregunta fundamental.

¿Fue esta solicitud/mensaje realmente creada por alguien que conoce la clave secreta de nuestro lado?

En otras palabras:

  • Solo quien conoce la clave HMAC puede generar una firma válida.
  • El servidor recibe payload + signature y verifica:
  • "¿La firma es correcta?"
  • "¿No se ha modificado en tránsito?"

Un mundo sin firmas HMAC

¿Qué pasa con REST/Webhook/API internas sin firmas HMAC?

  • Si un atacante conoce la estructura de la URL y los parámetros, puede
  • Generar solicitudes arbitrarias y hacer que el servidor las interprete como "peticiones internas naturales".

Por ejemplo:

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

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

Aunque originalmente sea un endpoint interno llamado solo desde la oficina administrativa, si la frontera de red se rompe, o hay una vulnerabilidad en el proxy interno, o se expone código de CI/CD, logs o documentación, el atacante puede simular ser un sistema interno.

En ese caso, si hubiera una firma HMAC, la situación cambiaría drásticamente.

  • Se adjunta una firma HMAC a todo el cuerpo de la solicitud.
  • El servidor verifica la firma en cada petición.

"Conocer la URL o los parámetros no basta; sin la clave, no se puede crear una solicitud válida."


Ejemplo: proteger acciones internas con firmas HMAC



Veamos un ejemplo sencillo (pseudo‑código con estilo TypeScript/Node.js).

1. Supongamos que el servidor tiene una clave secreta compartida

const HMAC_SECRET = process.env.HMAC_SECRET!; // Inyectada desde .env o Secret Manager

2. Cuando el cliente (o un servicio interno) crea la solicitud

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);

// Envío
fetch('/internal/run-action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Signature': signature,
  },
  body: JSON.stringify(payload),
});

3. Cuando el servidor valida

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' });
  }

  // Llegar aquí significa:
  // 1) El payload no fue alterado en tránsito
  // 2) La solicitud fue creada por alguien que conoce HMAC_SECRET
  handleInternalAction(req.body);
});

Esta estructura es el patrón básico que demuestra "¿Son estos datos realmente enviados por nosotros?".


Cuando hay varios operadores (administradores/desarrolladores): la rotación de claves HMAC es "no opcional, sino obligatorio"

Otro problema práctico de las claves HMAC es la persona.

  • Operadores, SRE, desarrolladores backend, contratistas, pasantes…
  • No todos tienen acceso a .env, pero la posibilidad de que la clave se exponga en GitOps, CI/CD, documentación, Slack, Notion, etc., siempre existe.

Lo importante es:

Actuar bajo la suposición de que la clave puede filtrarse.

Es decir, diseñar pensando que "en algún momento se filtrará".

Por eso la rotación de claves HMAC se vuelve obligatoria.

¿Por qué se necesita rotación?

  1. Las personas cambian * Salida, cambio de equipo, finalización de contratistas. * Quien vio la clave antes ya no debe poder verla.

  2. No se puede rastrear completamente la exposición * Mensajes de Slack, notas locales, capturas, memorias… * La afirmación "nunca filtramos la clave" es prácticamente imposible.

  3. Necesidad de recuperación después de un incidente * Si una clave anterior se filtró, se debe declarar que "a partir de ese momento solo se aceptan firmas con la nueva clave".

Patrón de rotación en la práctica

En organizaciones con varios operadores, se suele recomendar el siguiente patrón.

  1. Gestión de versiones de claves * HMAC_SECRET_V1, HMAC_SECRET_V2, etc. * Enviar kid (ID de clave) en el encabezado o payload.

  2. El servidor mantiene varias claves y las valida según la versión * Por ejemplo, validar V1 y V2, luego eliminar V1 después de un período.

  3. Documentar el proceso de rotación * "Crear nueva clave → desplegar → cambiar ambos lados (creador y validador) → eliminar la antigua". * Crear check‑list y runbook para que cualquier persona pueda seguirlo.

  4. Separar privilegios * Separar a quien crea/gestiona claves de quien modifica el código de la aplicación, cuando sea posible.

Cuando una clave HMAC se convierte en un valor fijo, la filtración de la clave equivale a la caída total del sistema. La rotación limita ese riesgo en el tiempo.


Zero Trust: "La confianza no es una estructura, sino algo que se construye en cada petición"

Finalmente, el incidente RCE de React nos recuerda un principio básico de seguridad.

La base de la seguridad es Zero Trust.

Zero Trust es simple:

  • "Es interno, entonces está bien."
  • "Es un valor usado solo en el frontend, entonces es seguro."
  • "Es nuestro propio framework, entonces se bloquea por defecto."

Estas afirmaciones deben prohibirse por defecto.

En su lugar, se deben hacer preguntas como:

  • "¿Se diseñó esta entrada bajo la suposición de que podría ser maliciosa?"
  • "¿Hay evidencia de que esta solicitud proviene realmente del actor que pretendemos?"
  • "¿Cómo verificamos que los datos no fueron alterados?"
  • "Si la clave se filtra, ¿qué alcance tiene y cómo la recuperamos?"

El incidente RCE también se ve bajo esta lente.

  • Los datos Flight de RSC se procesaron bajo la suposición de que "React los provee", lo que permitió que valores manipulables por el cliente afectaran la ruta de carga de módulos del servidor.

Desde la perspectiva Zero Trust, esta arquitectura habría sido un claro "señal de riesgo".

"Datos serializados manipulables por el cliente que influyen en la ruta de ejecución del código del servidor = señal de riesgo inminente."

Y la primera defensa que debe surgir es:

  • Firmas HMAC (integridad y origen)
  • Rotación de claves (limitar el alcance de una filtración)

Resumen: "No confíes, haz que lo prueben"

Los tres mensajes clave de este artículo son:

  1. No confíes en los datos por defecto * Incluso si el framework los usa internamente, si cruzan la red pueden convertirse en superficie de ataque.

  2. Las firmas HMAC son la herramienta básica para comprobar "¿Son estos datos realmente nuestros?" * Considera HMAC en todas las API REST, Webhooks, RPC internos y mensajes asíncronos. * En entornos con múltiples operadores, la rotación de claves debe ser un proceso obligatorio.

  3. La base de la seguridad es Zero Trust * La confianza no se otorga automáticamente; se construye con validaciones en cada petición y cada dato.

El incidente RCE de React nos recuerda que, en la era donde la frontera entre frontend y backend se difumina, debemos adoptar una postura de "no confíes, verifica".

"No confíes, verifica. Haz que la verificación sea parte del código, del proceso y de la política de gestión de claves."

El primer paso es la firma HMAC y la rotación de claves, sobre la cual se erige la filosofía de Zero Trust.

image