Divulgación · Escritura arbitraria en la nube

Cuando el cliente decide dónde guardar: subida arbitraria a un storage

Read in English

Un cliente tenía un problema de diseño que aparece una y otra vez en cualquier sitio que permita subir archivos: ¿quién decide dónde se guarda el archivo? Si la respuesta es "el navegador", cualquiera puede escribir donde no debería.

El punto de entrada

El frontend subía imágenes a un bucket de almacenamiento en la nube y lo hacía, como es habitual, con URLs prefirmadas: el navegador le pedía al backend una URL de subida válida por tiempo limitado y luego hacía un PUT directo al bucket. El endpoint era:

POST https://www.<dominio>/api/upload-media/get-presigned-urls
Content-Type: application/json

{"fileNames":["foto.jpg"],"contentTypes":["image/jpeg"],"bucketPath":"..."}

Ahí está el detalle: el cliente mandaba bucketPath, la carpeta de destino. Y el servidor lo usaba tal cual para construir la clave del objeto, sin validar nada: ni que la carpeta existiera, ni que perteneciera al usuario, ni siquiera que no contuviera ../.

Qué terminaba firmando el servidor

Con mi propia cuenta (registro gratuito) el servidor firmaba cualquier ruta que le pidiera:

bucketPath enviadoClave firmada
<carpeta><carpeta>/<ts>-test.html
compressed/<listing-de-otro-usuario>carpeta de otro vendedor
<carpeta>/<publicacion-ajena>carpeta de una publicación ajena
../../staticfirmada sin sanitizar

Y no restringía el Content-Type: podía pedir la firma para text/html. Los identificadores de publicaciones son públicos (aparecen en la propia web), así que apuntar a carpetas de terceros no requería adivinar nada.

La prueba de concepto

La prueba fue deliberadamente mínima y no destructiva: un único archivo HTML nuevo de 96 bytes, en una clave que no existía, sin sobrescribir nada.

# 1) Pedir la URL prefirmada para una carpeta arbitraria como text/html
curl -sk -b cookies.txt -H "Content-Type: application/json" \
  -d '{"fileNames":["test.html"],"contentTypes":["text/html"],"bucketPath":"<carpeta>"}' \
  "https://www.<dominio>/api/upload-media/get-presigned-urls"
# -> [{ "url": "https://<bucket>.s3.../<carpeta>/...-test.html?...", ... }]

# 2) Subir el archivo
curl -sk -X PUT -H "Content-Type: text/html" --data-binary @poc.html "URL_PREFIRMADA"
# -> HTTP 200

# 3) Verificar cómo se sirve
curl -sk -D - "https://assets.<dominio>/<carpeta>/...-test.html"
# -> HTTP/2 200
#    content-type: text/html        <-- se renderiza como página

El archivo quedaba servido como HTML ejecutable desde el subdominio de assets, el mismo que sirve todas las imágenes del sitio.

Impacto real

  • Phishing bajo marca oficial: una réplica del login servida desde el dominio legítimo, con certificado válido, es indistinguible de la real.
  • Inyección en carpetas ajenas: plantar archivos en publicaciones o perfiles de otros usuarios (IDs públicos).
  • Hosting de contenido arbitrario (malware, "facturas", QR fraudulentos) con URLs que parecen de la marca.
  • Abuso de costos: subidas masivas facturadas al bucket y a la CDN.

Lo que la vulnerabilidad no permitía

Un buen informe no infla el impacto. Verifiqué los límites reales:

  • No se podían sobrescribir objetos existentes: el servidor anteponía un <timestamp>- a la clave, y el bucket tenía versionado activo.
  • No se robaba sesión por XSS: las cookies eran host-only y HttpOnly, en un origen distinto del de assets.
  • Solo se firmaba PUT: no había lectura, borrado ni listado.
  • La carpeta compressed/ pasaba por un pipeline que re-encodaba la imagen y neutralizaba el HTML (solo esa carpeta).

Cómo se corrige

La raíz es una sola: el cliente no debe decidir la ruta. El servidor tiene que:

  1. Decidir el prefijo. Eliminar bucketPath del contrato o mapearlo server-side a un enum cerrado según el recurso y la sesión.
  2. Verificar propiedad del recurso antes de firmar.
  3. Restringir el Content-Type a imágenes y videos; rechazar text/html y image/svg+xml.
  4. Sanitizar el nombre de archivo y servir lo subido por usuarios desde un dominio separado con Content-Disposition: attachment.
← Volver a Seguridad