Media

ApostropheCMS: Prototype Pollution del lado del cliente en apos.http.parseQuery que deriva en XSS basado en DOM contra usuarios autenticados

  • Publicada: 20.10.2026
  • Actualizado: 20.10.2026 21:34

Resumen

La función auxiliar del lado del cliente apos.http.parseQuery() en ApostropheCMS construye un objeto anidado a partir de una cadena de consulta URL sin sanear las claves __proto__, constructor o prototype. El análisis de una cadena de consulta controlada por un atacante escribe, por tanto, propiedades arbitrarias en Object.prototype (prototype pollution del lado del cliente).

La interfaz de administración de ApostropheCMS invoca este analizador con window.location.search en cada carga de página autenticada (el inicializador data() de la barra de contexto de administración). Dado que la interfaz de administración está construida sobre Vue, una propiedad polucionada y enumerable en Object.prototype es capturada por la rutina estándar de montaje de elementos de Vue y se escribe en el DOM como una propiedad de elemento, transformando la prototype pollution en XSS basado en DOM dentro de la sesión autenticada de la víctima. El gadget de escalada es un comportamiento estándar y bien conocido de Vue; el fallo que se reporta es la fuente no saneada parseQuery junto con el activador autenticado, ambos parte del código de ApostropheCMS.

Cualquier atacante que logre que cualquier usuario conectado (desde el rol con menores privilegios guest hasta el rol con mayores privilegios admin) abra una URL manipulada puede ejecutar JavaScript arbitrario en la sesión de ese usuario.

Esto es diferente de CVE-2026-53609 (prototype pollution del lado del servidor mediante apos.util.set() / $pullAll, corregida en 4.31.0): lado distinto (cliente vs. servidor), ruta de código distinta y el código vulnerable aquí presente se encuentra en la versión actual.

Paquete

  • Ecosistema: npm
  • Paquete: apostrophe
  • Versiones afectadas: todas las versiones 4.x, desde 4.0.0 hasta 4.32.2 (actual latest). Comprobado mediante la inspección de los tarballs publicados en npm: apostrophe@4.0.0 ya contiene el descenso vulnerable (parentKey = 'root') sin filtrado de __proto__/constructor/prototype, y apostrophe@4.32.2 no presenta cambios al respecto. La lógica vulnerable parseQuery() se introdujo en el commit 0255086bf (2020-11-12), mucho antes de la versión 4.0, por lo que ApostropheCMS 3.x también está afectado (el mismo código se distribuye en 3.x en la ruta de compilación antigua modules/@apostrophecms/util/ui/public/2-http.js).
  • Versiones corregidas: ninguna al momento de redactar esto.

Detalles

El problema consta de tres partes. Las partes 1 y 2 corresponden al código de ApostropheCMS y constituyen la vulnerabilidad notificable. La parte 3 es el gadget de escalada, que es un comportamiento estándar de Vue y se incluye por exhaustividad.

1. La primitiva de polución: origen (ApostropheCMS)

modules/@apostrophecms/util/ui/src/http.js, función apos.http.parseQuery() (línea ~301) y su función interna patch() (líneas ~317–354):

js
function patch(key, value) {
  let match;
  let parentKey = 'root';
  let context = data;                    // data = {}  (ordinary object)
  key = decodeURIComponent(key);
  const path = key.split('[');
  path.forEach(function(subKey) {
    if (subKey === ']') {
      /* array handling */
    } else if (subKey.match(/^\d+]/)) {
      /* numeric index handling */
    } else {
      match = subKey.replace(']', '');
      if (!context[parentKey]) {         // (A) truthy check, not an own-property check
        context[parentKey] = {};
      }
      context = context[parentKey];      // (B) descends into whatever this resolves to
      parentKey = match;
    }
  });
  value = (value === null) ? value : decodeURIComponent(value);
  if (Array.isArray(context[parentKey])) {
    context[parentKey].push(value);
  } else if (context[parentKey] !== undefined) {
    context[parentKey] = [ context[parentKey], value ];
  } else {
    context[parentKey] = value;          // (C) assignment onto the resolved object
  }
}

No se rechaza ninguna clave __proto__, constructor o prototype. Al recorrer la entrada __proto__[x]=v:

  1. Segmento __proto__: en (A), context['__proto__'] se evalúa al prototipo heredado del objeto (truthy), por lo que se omite el inicializador {}; en (B), context se convierte en Object.prototype.
  2. Segmento x: en (C), Object.prototype['x'] = v.

La comprobación truthy en (A) (en lugar de una comprobación de propiedad propia) es lo que permite el descenso hacia el prototipo heredado. Dado que la asignación en (C) es una escritura de propiedad normal, la propiedad resultante Object.prototype es enumerable, lo cual es determinante para el gadget (3).

2. El activador: ejecutado en cada carga de página autenticada (ApostropheCMS)

modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue. El inicializador data() del componente (línea ~64) se ejecuta cuando se monta la barra de contexto de administración (es decir, en cada carga de página autenticada), y pasa la cadena de consulta sin procesar directamente al analizador vulnerable:

js
data() {
  const query = apos.http.parseQuery(location.search);   // <- attacker-controlled -> (1) pollution, on mount
  const draftMode = query.aposMode || 'published';
  // ...
}

El método refresh() del mismo componente (línea ~606) vuelve a pasar window.location.search a parseQuery() en eventos de refresco de la barra de administración y lo incorpora en doc._url (reanalizado dentro de apos.http.remote()), proporcionando puntos de activación adicionales.

La barra de contexto de administración solo se monta cuando existe una sesión y se carga el paquete de administración, por lo que nada de esto se ejecuta para visitantes anónimos; razón por la cual la polución se observa únicamente al haber iniciado sesión. Todo esto forma parte del código principal de ApostropheCMS (node_modules/apostrophe/...); no es introducido por ningún proyecto ni plantilla inicial.

3. El gadget de escalada: montaje de elementos en Vue (comportamiento del framework; conocido, no novedoso)

La carga útil demostrada poluciona la propiedad innerHTML (__proto__[innerHTML]=<img/src/onerror=alert(1)>). El receptor (sink) que la procesa es la ruta de montaje estándar de elementos de Vue, no el código de ApostropheCMS. Confirmado en el framework empaquetado (vue / @vue/runtime-core / @vue/runtime-dom, todos 3.5.42):

@vue/runtime-core mountElement, enumeración de propiedades (línea ~5741):

js
if (props) {
  for (const key in props) {                              // for...in walks the prototype chain
    if (key !== "value" && !shared.isReservedProp(key)) { // isReservedProp does NOT list innerHTML
      hostPatchProp(el, key, null, props[key], ...);      // props["innerHTML"] → inherited polluted value
    }
  }

@vue/runtime-dom patchDOMProp (línea ~583), alcanzado mediante hostPatchProp:

js
function patchDOMProp(el, key, value, ...) {
  if (key === "innerHTML" || key === "textContent") {
    if (value != null) {
      el[key] = key === "innerHTML" ? unsafeToTrustedHTML(value) : value;  // el.innerHTML = payload
    }
    return;
  }

Mecanismo: cada vez que Vue monta un elemento que tiene un objeto props no nulo sin su propio innerHTML (habitual durante el renderizado de administración), for (const key in props) enumera la propiedad innerHTML heredada y enumerable de Object.prototype; supera los filtros "value" y isReservedProp; props["innerHTML"] resuelve a la carga útil del atacante; y patchDOMProp la escribe en el.innerHTML. unsafeToTrustedHTML es el contenedor de Trusted Types de Vue, no un sanitizador; es un intermediario transparente a menos que se aplique Trusted Types (lo cual no está habilitado por defecto).

Este gadget es un método bien documentado y prácticamente universal para escalar de prototype pollution en el cliente a XSS en aplicaciones Vue. No se reclama como un hallazgo novedoso ni le corresponde a ApostropheCMS corregirlo, ya que no puede eliminarse sin modificar Vue. Su importancia aquí radica en que permite explotar de forma fiable la polución (1) en cualquier sesión de administración de ApostropheCMS.

Prueba de concepto

Requisito previo: una instancia de ApostropheCMS en ejecución y cualquier sesión autenticada (basta con una cuenta guest). La persona de prueba o víctima debe haber iniciado sesión.

A. Prototype pollution (verificable en consola):

  1. Iniciar sesión con cualquier usuario.
  2. Navegar a: https://TARGET/?__proto__[prop]=polluted
  3. En la consola del navegador: Object.prototype.prop > devuelve "polluted".

B. XSS basado en DOM:

  1. Iniciar sesión con cualquier usuario.
  2. Navegar a: https://TARGET/?__proto__[innerHTML]=%3Cimg/src/onerror%3dalert(1)%3E
  3. alert(1) se ejecuta en el origen autenticado de la víctima.

Para capturar la pila de ejecución: DevTools > Sources > abrir el bundle de administración > formatear (pretty-print) > buscar unsafeToTrustedHTML (o === "innerHTML") > colocar un punto de interrupción en la rama innerHTML de patchDOMProp > recargar la URL de la carga útil con la sesión iniciada > inspeccionar value y recorrer la pila de llamadas a través de hostPatchProp hasta for…in de mountElement.

Tanto A como B se reprodujeron en ApostropheCMS 4.32.2 (Vue 3.5.42) en los navegadores Chrome, Firefox y Edge.

Impacto

Cualquier atacante que logre inducir a un usuario autenticado de ApostropheCMS a abrir una URL manipulada puede ejecutar JavaScript arbitrario en su sesión. Como el gadget de escalada es un comportamiento inherente de Vue, se trata de una vulnerabilidad explotable de manera consistente y no solo teórica. Las consecuencias varían según el rol de la víctima:

  • Cualquier víctima autenticada: acciones y lectura de datos permitidos para esa cuenta; robo de la cookie de sesión / token CSRF del contexto autenticado.
  • Víctima con rol de editor/administrador: el script inyectado puede interactuar con las API REST administrativas autenticadas como la víctima, creando o modificando documentos y cuentas de usuario, alterando la configuración y pudiendo derivar en la toma de control de cuentas y en un posible compromiso total de la instancia del CMS.

La prototype pollution también puede afectar a otras rutas de código no relacionadas que lean propiedades heredadas, por lo que podrían existir otros gadgets aparte del sumidero demostrado en Vue innerHTML.

Remediación

La corrección corresponde a ApostropheCMS, en el origen (1). No se puede depender de que el gadget (3) cambie en el framework, por lo que sanear parseQuery es la solución adecuada.

Corrección principal: rechazar claves que afecten al prototipo en parseQuery/patch():

js
apos.http.parseQuery = function(query) {
  query = query.replace(/^\?/, '');
  const data = {};
  const DANGEROUS = new Set([ '__proto__', 'constructor', 'prototype' ]);
  query.split('&').forEach(function(pair) {
    // ... unchanged ...
  });
  return data.root || {};

  function patch(key, value) {
    let match;
    let parentKey = 'root';
    let context = data;
    key = decodeURIComponent(key);
    const path = key.split('[');
    let dangerous = false;
    path.forEach(function(subKey) {
      const cleanKey = subKey.replace(']', '');
      if (DANGEROUS.has(cleanKey)) {   // refuse to traverse into prototype-affecting keys
        dangerous = true;
        return;
      }
      // ... existing subKey handling ...
    });
    if (dangerous) {
      return;                          // discard this parameter entirely
    }
    // ... existing assignment ...
  }
};

Defensa en profundidad:

  • Reemplazar la comprobación truthy en (A) por una comprobación de propiedad propia: if (!Object.prototype.hasOwnProperty.call(context, parentKey)), para que una clave nunca pueda descender a un prototipo heredado.
  • Crear data y cualquier contenedor intermedio con Object.create(null) para que no tengan cadena de prototipos que alcanzar.

Mitigación provisional para administradores: restringir a quién se le asignan cuentas autenticadas y tener precaución de no abrir enlaces no fiables mientras se tenga una sesión abierta. No existe ningún ajuste de configuración en el servidor que deshabilite este comportamiento vulnerable en el cliente.

Referencias

  • Origen (ApostropheCMS): apostrophe/modules/@apostrophecms/util/ui/src/http.js — parseQuery() / patch()
  • Activador (ApostropheCMS): apostrophe/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue — data() (~línea 64), refresh() (~línea 606)
  • Gadget de escalada (Vue 3.5.42, comportamiento del framework): @vue/runtime-core mountElement enumeración de props (~línea 5741); rama innerHTML de @vue/runtime-dom patchDOMProp (~línea 583)
  • Relacionado pero distinto: CVE-2026-53609 / GHSA-6h5j-32cf-4253 (prototype pollution del lado del servidor vía apos.util.set() / $pullAll, corregida en 4.31.0)
  • CWE-1321; CWE-79

Créditos

Usuario de GitHub: Xx-LoGiCaL-xX

Qué hacer

La corrección debe aplicarse en el origen dentro de ApostropheCMS (1) saneando parseQuery.

Corrección principal: rechazar claves que afecten al prototipo en parseQuery/patch():

js
apos.http.parseQuery = function(query) {
  query = query.replace(/^\?/, '');
  const data = {};
  const DANGEROUS = new Set([ '__proto__', 'constructor', 'prototype' ]);
  query.split('&').forEach(function(pair) {
    // ... unchanged ...
  });
  return data.root || {};

  function patch(key, value) {
    let match;
    let parentKey = 'root';
    let context = data;
    key = decodeURIComponent(key);
    const path = key.split('[');
    let dangerous = false;
    path.forEach(function(subKey) {
      const cleanKey = subKey.replace(']', '');
      if (DANGEROUS.has(cleanKey)) {   // refuse to traverse into prototype-affecting keys
        dangerous = true;
        return;
      }
      // ... existing subKey handling ...
    });
    if (dangerous) {
      return;                          // discard this parameter entirely
    }
    // ... existing assignment ...
  }
};

Defensa en profundidad:

  • Sustituir la comprobación truthy en (A) por if (!Object.prototype.hasOwnProperty.call(context, parentKey)).
  • Construir data y los contenedores con Object.create(null).

Mitigación provisional: Restringir la concesión de cuentas autenticadas y evitar abrir enlaces no fiables con la sesión iniciada. No existe una opción de configuración en el servidor para desactivar el comportamiento cliente vulnerable.

Software afectado

Haz clic en una tecnología para ver todas sus vulnerabilidades.

PlataformaPaqueteVersiones vulnerablesCorregida en
Paquete de npm apostrophe < 4.33.0 4.33.0

Referencias

Puntuación CVSS 6.1/10
Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Debilidad (CWE)
CWE-1321, CWE-79
Plataforma
npm
Paquete
apostrophe
Publicada
20.10.2026
Actualizado
20.10.2026 21:34

Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.

OSV / GitHub Advisory Database - GHSA-m2m5-rw3w-cwmj - CC BY 4.0

Historial de vulnerabilidades: apostrophe

Todas las vulnerabilidades publicadas de este paquete (Paquete de npm), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.

Todas (27) en el archivo
Entradas en total
27
Primera → última
09.2020 10.2026
Sin corrección
4
Explotada en ataques
0

Distribución por gravedad

  • Crítica 2
  • Alta 8
  • Media 13
  • Baja 4

Divulgaciones por año

  1. 2020: 2
  2. 2021: 2
  3. 2022: 0
  4. 2023: 0
  5. 2024: 0
  6. 2025: 0
  7. 2026: 23
Gravedad:
  1. ApostropheCMS: Prototype Pollution del lado del cliente en apos.http.parseQuery que deriva en XSS basado en DOM contra usuarios autenticados esta entrada
    Media · 6.1 CVE-2026-113659 < 4.33.0 4.33.0
  2. Alta · 7.3 CVE-2026-45011 = 4.29.0 sin corrección
  3. Crítica · 9.8 CVE-2021-25979 >= 2.63.0, < 3.4.0 3.4.0

Identifica las vulnerabilidades de tu aplicación

Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.

Conectar tu aplicación
Newsletter DockRay

La seguridad de tus aplicaciones en tu correo

Vulnerabilidades importantes, recomendaciones concretas y consejos prácticos para tus aplicaciones. Elige tus tecnologías y sigue lo que importa para tu proyecto.

  • Un resumen diario de nuevas vulnerabilidades, también las críticas.
  • Avisos sobre actualizaciones importantes.
  • Consejos prácticos sobre seguridad y monitorización de aplicaciones.

Déjalo vacío para recibir avisos de todas las tecnologías.

Suscripción gratuita. Date de baja con un clic en cualquier correo.