El Payload Que Nunca Inspeccionaste

¡Hola Devs! Abre la pestaña Network en cualquier página del Next.js App Router y filtra por Content-Type: text/x-component. Lo que ves no es JSON. Es Flight — el protocolo de streaming line-delimited de React, con su propio sistema de tipos, su propia resolución de referencias y sus propias reglas para reconstruir comportamiento ejecutable en el cliente. 🚀

La mayoría de los devs React nunca mira. El framework lo maneja, así que nadie cuestiona lo que esa confianza implica. Luego en diciembre de 2025, cayó CVE-2025-55182 — un RCE no autenticado de CVSS 10.0 en la capa de deserialización de Flight. CISA lo agregó al catálogo de vulnerabilidades explotadas. Sysdig vinculó la explotación in-the-wild con actores estado-nación de Corea del Norte desplegando implantes file-less vía blockchain Ethereum.

Esto no es un bug de parsing aislado. Es un síntoma. Flight reconstruye referencias ejecutables, componentes lazy-loaded, endpoints RPC y estado asíncrono desde un stream de texto. Eso es un sistema de deserialización — y hereda todos los riesgos clásicos de deserialización.

Security researcher analyzing React Flight protocol payload in browser network tab for deserialization vulnerabilities Algorithm Concept Visual

Flight On The Wire: El Sistema de Prefijo $

Cada línea de Flight sigue ID:TAG:PAYLOAD. Lo interesante es el sistema de prefijo $ — cuando el parser del cliente encuentra un string que empieza con $, no lo trata como texto literal. Lo rutea por un camino de resolución específico en parseModelString.

PrefijoTipoComportamiento del Parser
$Model ReferenceResuelve a otro chunk en el stream
$:Property AccessNavega en propiedades de un chunk resuelto ($1:user:name)
$FServer ReferenceRepresenta una Server Action invocable (RPC)
$LLazy ComponentDifiere el loading del componente
$@Raw ChunkRetorna el wrapper interno Chunk (Thenable)
$BBlob/BinaryDispara el handler de blob

El $: es el crítico para explotación. Permite property traversal arbitrario dirigido por datos en el stream. Si has auditado JavaScript para prototype pollution, ese patrón te suena familiar.

La Gadget Chain de React2Shell

La causa raíz estaba en getOutlinedModel, la función que resuelve paths profundos de referencias $::

// Loop vulnerable — sin check de hasOwnProperty
for (key = 1; key < reference.length; key++)
  parentObject = parentObject[reference[key]];

El atacante manda $1:__proto__:constructor:constructor, y el loop sube de un objeto JSON plano hasta Object.prototype, luego Function. Function("código arbitrario")() ejecuta. Sin allowlist, sin check de __proto__.

Desde ahí, la chain compone features legítimas de Flight:

  1. Prototype walk a Function vía $:
  2. Raw chunk self-reference vía $@0 para obtener un handle interno mutable
  3. Thenable hijack — setea .then a Chunk.prototype.then
  4. Context confusion — sobrescribe _response._formData.get apuntando a Function
  5. Trigger vía $B0 — blob handler llama Function("comando_atacante")()

Cada paso es una feature legítima. La vulnerabilidad emerge de la composición. ¡Fíjate en esto! 👀

Node.js server processing streaming React Server Component chunks with prototype pollution attack vectors highlighted Dev Environment Setup

Defensas, Rankeadas por Impacto

1. Validación de Schema en Cada Server Action

La defensa más impactante a nivel de aplicación. Valida con Zod o Valibot al inicio de cada Server Action — antes de loggear, antes de cualquier lógica. Usa .safeParse(), no .parse().

"use server"
import { z } from "zod"

const CommentSchema = z.object({
  postId: z.string().uuid(),
  body: z.string().min(1).max(5000),
})

// Bien: valida el argumento crudo primero
export async function addComment(data) {
  const parsed = CommentSchema.safeParse(data)
  if (!parsed.success) return { error: "Invalid input" }
  await db.comments.create(parsed.data)
}

Si tu Server Action no empieza con un schema parse, trátalo como vulnerabilidad. Considera una regla ESLint custom que flaggee cualquier export "use server" sin validación en la primera instrucción.

2. El Paquete server-only

Importa server-only al inicio de cualquier archivo con credenciales de DB o lógica interna. El build falla si un Client Component lo importa transitivamente. Cuidado con barrel files — rompen la frontera silenciosamente.

3. CSRF Más Allá de los Defaults del Framework

CVE-2026-27978 mostró que el check Origin vs Host de Next.js tiene edge cases (bypass de Origin: null vía iframes sandboxed). Setea SameSite=Strict en cookies de sesión, agrega tokens CSRF por sesión para operaciones críticas, y nunca pongas "null" en allowedOrigins.

4. Verifica Tu Versión del Patch

El fix del RCE llegó en React 19.0.1, 19.1.2, 19.2.1. Fixes de DoS necesitan 19.0.4+, 19.1.5+ o 19.2.4+. Checa tu lockfile:

npm ls react react-dom react-server-dom-webpack

5. Taint API — Guardrail de Dev, No Frontera de Seguridad

taintObjectReference atrapa errores honestos (pasar user object completo al Client Component). Pero taint rastrea referencias, no contenido — cualquier spread o round-trip de serialización lo rompe. Defensa en profundidad útil, no frontera primaria.

6. WAFs — Reducción de Ruido, No Frontera

WAFs atrapan scanners automatizados. No paran atacantes motivados: buffers de inspección son ~128KB, así que prepende 130KB de padding y el payload pasa. Trata WAF como reducción de ruido, no seguridad.

Lo Que Sigue Estructuralmente Expuesto

  • MITM en el Flight stream — texto plano, estructura predecible. Inyecta imports $I, server references $F, o modifica data rows D.
  • Enumeración de Server Actions — server-reference-manifest.json expuesto entrega un mapa completo de API para ataques IDOR.
  • Tampering de closures encriptadas — NEXT_SERVER_ACTIONS_ENCRYPTION_KEY estático en multi-server + file read = closures forjadas.
  • Activación de supply chain — paquetes npm dormantes bundled pero nunca cargados pueden activarse vía referencias $I inyectadas.

Developer reviewing ReactFlightClient.js source code to understand Flight protocol deserialization sinks Technical Structure Concept

Hacia Dónde Va Esto

El Flight protocol resuelve un problema genuinamente difícil: streaming de árboles de componentes interactivas con hidratación progresiva y code splitting server-driven. Funciona. Pero funciona serializando referencias ejecutables en un protocolo de texto y confiando en la estructura del stream de ambos lados.

El patch de hasOwnProperty está correcto. Los fixes de DoS están en su lugar. Pero exponer property traversal arbitrario y reconstrucción de Thenables ejecutables a través de un protocolo de red fue un error de diseño. El patch trata el síntoma.

Esperar que el parser maneje cada edge case no ha funcionado históricamente — GWT, JSF ViewState, ASP.NET ViewState siguieron el mismo patrón. Si shipeas Server Components, lee react-client/src/ReactFlightClient.js. Sabe qué confía tu framework en tu nombre.

Lectura complementaria:

Fuente: Smashing Magazine — Weaponizing and Defending the React Flight Protocol

Este contenido fue redactado con la asistencia de herramientas de IA, basándose en fuentes confiables, y fue revisado por nuestro equipo editorial antes de su publicación. No reemplaza el asesoramiento de un profesional especializado.