Media

ApostropheCMS: la cabecera Host controla el origen de peticiones de imágenes en @apostrophecms/ai-helper, permitiendo SSRF autenticado

  • Publicada: 20.10.2026
  • Actualizado: 20.10.2026 21:29

Resumen

El paquete @apostrophecms/ai-helper construye la URL para la obtención de imágenes en el servidor resolviendo una ruta relativa al almacenamiento frente a req.baseUrl. Cuando el sitio no define una URL base global (comportamiento por defecto), req.baseUrl se deriva de la cabecera HTTP Host entrante. Un editor autenticado puede generar una imagen de IA en base64 (que se almacena sin un url guardado) y luego enviar una solicitud de aceptación PATCH no autorizada con una cabecera Host elegida por el atacante. El servidor emite entonces una petición saliente GET hacia el origen seleccionado por el atacante, generando una primitiva de Server-Side Request Forgery (SSRF) que alcanza hosts y puertos a los que el cliente no podría acceder de otro modo. El código afectado se encuentra en packages/ai-helper/modules/@apostrophecms/ai-helper-image/index.js.

Causa Raíz

La ruta de aceptación de imágenes confía en req.baseUrl como un origen seguro para construir un objetivo interno de obtención, pero req.baseUrl está influenciado por el atacante siempre que el sitio no tenga una URL base configurada. El generador de URLs concatena una ruta de almacenamiento local predeterminada (que es relativa, por ejemplo /uploads) y la resuelve frente a req.baseUrl; dado que el operando izquierdo es relativo, el origen resultante se toma en su totalidad de req.baseUrl. Dos protecciones presentes en otros lugares no existen en esta ruta: la ruta de generación exige privilegios de edición, pero la de aceptación no realiza comprobaciones de autorización ni de pertenencia, y no hay listas de permitidos ni validaciones que restrinjan el origen resuelto antes de la petición.

packages/ai-helper/modules/@apostrophecms/ai-helper-image/index.js
  aiHelperImageUrl (line 91-92): image.url || new URL(self.uploadfs.getUrl() + `/ai-helper-images/${image._id}.png`, req.baseUrl)
  aiHelperFetchImage (line 64-65): fetch(self.aiHelperImageUrl(req, image))
  apiRoutes.patch['ai-helper/:_id'] (line 314-333): findOne({ _id }) only; no permission or ownership check
packages/apostrophe/modules/@apostrophecms/i18n/index.js
  setPrefixUrls (line 799-820): req.baseUrl falls back to `${req.protocol}://${host}`, host from the Host header when apos.baseUrl is unset

Impacto

Un atacante con una cuenta de editor con bajos privilegios (la capacidad necesaria para generar una imagen por IA, que inserta un registro base64 sin un url almacenado y devuelve su identificador) puede inducir al servidor a emitir peticiones salientes GET a un origen de su elección proporcionando una cabecera Host manipulada en la solicitud de aceptación. Esto da lugar a SSRF: sondeo de hosts y puertos internos inaccesibles desde la red del atacante y una exfiltración semiciega limitada donde una respuesta parseable como imagen en la ruta fija se convierte en un adjunto visible. La ruta de la petición está fija en /uploads/ai-helper-images/<id>.png, por lo que únicamente el origen (host, puerto y esquema http) queda bajo control del atacante; no es posible obtener rutas arbitrarias como los metadatos de proveedores en la nube a través de esta primitiva. No se demostró afectación a la integridad ni a la disponibilidad. La explotación requiere el despliegue predeterminado sin URL base global y con almacenamiento local de archivos, junto a un proveedor configurado que devuelva datos de imagen en base64.

Prueba de Concepto

Preconditions:
  - ApostropheCMS site with @apostrophecms/ai-helper installed and a configured
    base64-returning image provider.
  - No global base URL configured (apos.baseUrl unset), default local uploadfs.
  - Attacker holds an account with edit capability on at least one content type.
  1. Autenticarse como editor y generar una imagen de IA en base64: POST /api/v1/@apostrophecms/image/ai-helper con el cuerpo {"prompt":"x"}. La respuesta contiene el nuevo identificador de registro <_id>. El registro persistido no tiene campo url.
  2. Enviar la solicitud de aceptación con una cabecera Host maliciosa:
PATCH /api/v1/@apostrophecms/image/ai-helper/<_id> HTTP/1.1
Host: 169.254.169.254
Content-Type: application/json

{"accepted": true}
  1. El servidor deriva req.baseUrl como http://169.254.169.254, genera el objetivo de fetch http://169.254.169.254/uploads/ai-helper-images/<_id>.png y emite la petición saliente.
Derivation confirmed against source logic:
  base64 record (no url) + Host: 169.254.169.254
    -> new URL("/uploads/ai-helper-images/<_id>.png", "http://169.254.169.254")
    -> server fetches: http://169.254.169.254/uploads/ai-helper-images/<_id>.png
  control (apos.baseUrl set to https://cms.example.com):
    -> fetch target uses https://cms.example.com (Host ignored)
  control (record stores its own url):
    -> aiHelperImageUrl short-circuits to the stored url (no SSRF)

Cadena de Ataque

  1. Exposición: El módulo @apostrophecms/ai-helper-image registra las rutas POST y PATCH bajo /api/v1/@apostrophecms/image/ai-helper, accesibles por cualquier sesión autenticada en un sitio que tenga instalado el paquete.
  2. Control: El atacante controla la cabecera HTTP Host en la petición PATCH; el middleware global locale la copia en req.baseUrl debido a que apos.baseUrl no está configurado (packages/apostrophe/modules/@apostrophecms/i18n/index.js:805,816-819).
  3. Ruta: El atacante primero genera una imagen base64 mediante POST, que inserta un registro sin un url (ai-helper-image/index.js:282-284); el identificador devuelto se utiliza luego en la petición de aceptación PATCH, que invoca a aiHelperFetchImage (:332).
  4. Protección: El manejador PATCH solo comprueba req.body.accepted y la existencia del registro mediante findOne({ _id }) (:318,:322); no comprueba permisos ni asignación de propietario, a diferencia de la ruta POST que llama a checkPermissions (:176). Ninguna validación restringe el origen resuelto antes de la obtención.
  5. Primitiva: aiHelperImageUrl resuelve la ruta de almacenamiento relativa frente a req.baseUrl derivado de Host y aiHelperFetchImage invoca fetch sobre el resultado (:91-92,:64-65).
  6. Resultado: El servidor realiza una petición saliente GET al origen elegido por el atacante en una ruta fija, permitiendo alcanzar hosts y puertos internos y obteniendo respuestas limitadas.

Evidencia de Evasión

La protección checkPermissions (ai-helper-image/index.js:176) de la ruta de generación no está en la ruta de aceptación, por lo que ninguna autorización protege la petición. Se probaron y refutaron los siguientes supuestos:

  • El almacenamiento local predeterminado devuelve una URL relativa: uploadfs.getUrl() genera /uploads porque el backend local construye uploadsUrl = (apos.baseUrl || '') + prefix + '/uploads', de modo que new URL(relative, req.baseUrl) toma su origen de req.baseUrl. Configurar una URL base o una URL de almacenamiento absoluta (S3/CDN) neutraliza el ataque, razón por la cual se consideran controles negativos y no la configuración estándar.
  • El registro base64 no tiene url persistido: insertOne(image) almacena el registro antes de asignar image.url en el objeto de respuesta en memoria (:282-284 frente a :294), por lo que el manejador PATCH vuelve a leer un registro sin url y el atajo en aiHelperImageUrl no se aplica.
  • Propiedad y enumeración: GET y DELETE en estos registros tienen ámbito de userId y los identificadores cuid2 no se pueden adivinar, por lo que el actor práctico es el editor generador; esto limita el privilegio necesario a un usuario autenticado de bajos privilegios.
  • No es un duplicado: la corrección del aviso de SSRF por pretty-URL reside enteramente en el módulo independiente @apostrophecms/file y su proxy de flujo, sin tocar packages/ai-helper; el aviso de SSRF en importación de texto enriquecido utiliza otra ruta con lista de permitidos. No existe ningún aviso previo para @apostrophecms/ai-helper. Es un punto vulnerable paralelo.

Versiones Afectadas

  • Ecosystem: npm
  • Package: @apostrophecms/ai-helper
  • Confirmed affected range: >= 1.0.0-beta.10, <= 1.0.0-beta.12
  • Latest release checked: 1.0.0-beta.12 (npm registry, dist-tag latest)
  • Fix status: not fixed

El patrón vulnerable se introdujo en 1.0.0-beta.10: las versiones anteriores hasta 1.0.0-beta.9 obtenían directamente la URL del proveedor almacenada (fetch(url)) sin resolución con req.baseUrl y sin construcción relativa a uploadfs, por lo que no están afectadas. La inspección de los paquetes publicados para 1.0.0-beta.10, 1.0.0-beta.11 y 1.0.0-beta.12 confirma que la cadena completa está presente. Este paquete solo se publica en el canal beta.

Corrección Sugerida

No tratar req.baseUrl como un origen de confianza para peticiones en el servidor. Aplicar en la ruta de aceptación la misma autorización que en la ruta de generación y añadir una verificación de propiedad. De forma independiente, restringir el destino de la petición: resolver la ruta de almacenamiento contra una base configurada en el servidor en lugar de usar req.baseUrl derivado de la solicitud, y validar o filtrar con lista de permitidos cualquier origen resultante antes de invocar fetch. Asegurar que la corrección cubra tanto las representaciones base64 como las de URL almacenada, así como cualquier invocador hacia aiHelperImageUrl o aiHelperFetchImage. Como mitigación provisional, configurar una URL base global y aplicar reglas de salida de red en el servidor para evitar peticiones a endpoints internos sensibles.

Reportado por zx (GitHub: @manus-pi).

Qué hacer

Actualice a la versión 1.0.0-beta.13 o superior. Como mitigación alternativa, configure una URL base global y restrinja las peticiones salientes hacia direcciones internas sensibles.

Software afectado

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

PlataformaPaqueteVersiones vulnerablesCorregida en
Paquete de npm @apostrophecms/ai-helper >= 1.0.0-beta.10, < 1.0.0-beta.13 1.0.0-beta.13

Referencias

Puntuación CVSS 4.3/10
Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Debilidad (CWE)
CWE-918
Plataforma
npm
Paquete
@apostrophecms/ai-helper
Publicada
20.10.2026
Actualizado
20.10.2026 21:29

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

OSV / GitHub Advisory Database - GHSA-527g-ff96-x6wj - CC BY 4.0

Historial de vulnerabilidades: @apostrophecms/ai-helper

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.

Esta es la única vulnerabilidad conocida de este paquete.

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.