ApostropheCMS: la cabecera Host controla el origen de peticiones de imágenes en @apostrophecms/ai-helper, permitiendo SSRF autenticado
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.
- Autenticarse como editor y generar una imagen de IA en base64:
POST /api/v1/@apostrophecms/image/ai-helpercon el cuerpo{"prompt":"x"}. La respuesta contiene el nuevo identificador de registro<_id>. El registro persistido no tiene campourl. - Enviar la solicitud de aceptación con una cabecera
Hostmaliciosa:
PATCH /api/v1/@apostrophecms/image/ai-helper/<_id> HTTP/1.1
Host: 169.254.169.254
Content-Type: application/json
{"accepted": true}
- El servidor deriva
req.baseUrlcomohttp://169.254.169.254, genera el objetivo de fetchhttp://169.254.169.254/uploads/ai-helper-images/<_id>.pngy 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
- Exposición: El módulo
@apostrophecms/ai-helper-imageregistra las rutasPOSTyPATCHbajo/api/v1/@apostrophecms/image/ai-helper, accesibles por cualquier sesión autenticada en un sitio que tenga instalado el paquete. - Control: El atacante controla la cabecera HTTP
Hosten la peticiónPATCH; el middleware globallocalela copia enreq.baseUrldebido a queapos.baseUrlno está configurado (packages/apostrophe/modules/@apostrophecms/i18n/index.js:805,816-819). - Ruta: El atacante primero genera una imagen base64 mediante
POST, que inserta un registro sin unurl(ai-helper-image/index.js:282-284); el identificador devuelto se utiliza luego en la petición de aceptaciónPATCH, que invoca aaiHelperFetchImage(:332). - Protección: El manejador
PATCHsolo compruebareq.body.acceptedy la existencia del registro mediantefindOne({ _id })(:318,:322); no comprueba permisos ni asignación de propietario, a diferencia de la rutaPOSTque llama acheckPermissions(:176). Ninguna validación restringe el origen resuelto antes de la obtención. - Primitiva:
aiHelperImageUrlresuelve la ruta de almacenamiento relativa frente areq.baseUrlderivado de Host yaiHelperFetchImageinvocafetchsobre el resultado (:91-92,:64-65). - Resultado: El servidor realiza una petición saliente
GETal 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/uploadsporque el backend local construyeuploadsUrl = (apos.baseUrl || '') + prefix + '/uploads', de modo quenew URL(relative, req.baseUrl)toma su origen dereq.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
urlpersistido:insertOne(image)almacena el registro antes de asignarimage.urlen el objeto de respuesta en memoria (:282-284frente a:294), por lo que el manejadorPATCHvuelve a leer un registro sinurly el atajo enaiHelperImageUrlno se aplica. - Propiedad y enumeración:
GETyDELETEen estos registros tienen ámbito deuserIdy 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/filey su proxy de flujo, sin tocarpackages/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: npmPackage: @apostrophecms/ai-helperConfirmed affected range: >= 1.0.0-beta.10, <= 1.0.0-beta.12Latest 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.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Paquete de npm | @apostrophecms/ai-helper | >= 1.0.0-beta.10, < 1.0.0-beta.13 | 1.0.0-beta.13 |
Referencias
- https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-527g-ff96-x6wj
- 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/@apostrophecms/ai-helper@1.0.0-beta.13
- 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.