ApostropheCMS: Prototype Pollution del lado del cliente en apos.http.parseQuery que deriva en XSS basado en DOM contra usuarios autenticados
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.0ya contiene el descenso vulnerable (parentKey = 'root') sin filtrado de__proto__/constructor/prototype, yapostrophe@4.32.2no presenta cambios al respecto. La lógica vulnerableparseQuery()se introdujo en el commit0255086bf(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 antiguamodules/@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):
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:
- Segmento
__proto__: en (A),context['__proto__']se evalúa al prototipo heredado del objeto (truthy), por lo que se omite el inicializador{}; en (B),contextse convierte enObject.prototype. - 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:
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):
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:
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):
- Iniciar sesión con cualquier usuario.
- Navegar a:
https://TARGET/?__proto__[prop]=polluted - En la consola del navegador:
Object.prototype.prop> devuelve"polluted".
B. XSS basado en DOM:
- Iniciar sesión con cualquier usuario.
- Navegar a:
https://TARGET/?__proto__[innerHTML]=%3Cimg/src/onerror%3dalert(1)%3E 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():
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
datay cualquier contenedor intermedio conObject.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-coremountElementenumeración de props (~línea 5741); rama innerHTML de@vue/runtime-dompatchDOMProp(~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():
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
datay los contenedores conObject.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.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Paquete de npm | apostrophe | < 4.33.0 | 4.33.0 |
Referencias
- https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-m2m5-rw3w-cwmj
- https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce
- https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350
- https://github.com/apostrophecms/apostrophe
- https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.33.0
- 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.
- 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
- 2020: 2
- 2021: 2
- 2022: 0
- 2023: 0
- 2024: 0
- 2025: 0
- 2026: 23
-
ApostropheCMS: Prototype Pollution del lado del cliente en apos.http.parseQuery que deriva en XSS basado en DOM contra usuarios autenticados esta entrada
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.