Files
Musify/docs/presigned-upload-hardening.md
T
2026-05-14 14:32:19 +02:00

3.5 KiB
Raw Blame History

Hardening: abuso de pre-signed uploads (Opción B)

Contexto

Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando pictureName/audioName.

Esto mejora latencia y descarga la API, pero abre un vector de abuso: un usuario puede llamar muchas veces a RequestPlayListPictureUpload / RequestTrackUploadUrls y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”).

Qué pasa hoy si el usuario spamea

  • Se generan muchas URLs pre-firmadas (costo bajo pero no cero).
  • El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB.
  • El impacto principal es:
    • coste/uso de S3 (storage + requests)
    • posible saturación del endpoint de generación de URLs

Objetivo

Reducir abuso y coste, manteniendo:

  • UX simple
  • backend sin streams
  • workflows asíncronos + MassTransit Outbox

Mitigaciones recomendadas (pueden coexistir)

1) Rate limiting por usuario/IP (primera línea de defensa)

Aplicar limitación en el host/API (no en Application). Ejemplos:

  • N requests/min por UserId
  • opcional: M requests/min por IP

Ventajas:

  • Corta abuso sin tocar DB/S3.
  • No cambia dominio.

Nota: requiere acceso al proyecto host (controllers/minimal API) para aplicarlo.

2) “Upload intents” en DB (control y conciliación)

Persistir una intención de upload cuando se genera un pre-signed URL.

Modelo sugerido (mínimo):

  • UploadIntent
    • Id
    • UserId
    • Bucket
    • Key
    • ContentType
    • ExpiresAt
    • Status: Issued | Consumed | Expired
    • CreatedAt

Reglas:

  • Al pedir URL (RequestUpload):
    • insertar intent Issued
    • imponer límite por usuario: p.ej. MaxActiveIntentsPerUser = 5
    • si supera el límite: devolver 429 Too Many Requests (o Conflict) según convención del API
  • Al crear/actualizar entidad (Create*/Update* V2):
    • validar que el pictureName/audioName recibido corresponde a un intent Issued del mismo UserId y no expirado
    • marcar intent como Consumed

Ventajas:

  • Previene spam con reglas duras y auditables.
  • Permite cleanup de huérfanos con certeza.

Coste:

  • entidad + migración + lógica de validación.

3) Limitar el scope del pre-signed URL (reducción de daño)

Aplicar restricciones conservadoras:

  • Reducir TTL:
    • ExpiresInSeconds más bajo (p.ej. 60–120s en vez de 600s)
  • Validar/limitar:
    • ContentType permitido
    • extensiones permitidas (FileType)
  • Usar keys temporales por usuario:
    • PlayLists/TempUploads/{userId}/{guid}.{ext}
    • Tracks/TempUploads/{userId}/{guid}.{ext}

Luego, en el workflow/worker:

  • mover/copiar de TempUploads/... a la ruta final (OriginalPictures/... / OriginalAudios/...)
  • borrar el objeto temporal

Ventajas:

  • Cleanup masivo sencillo (por prefijo TempUploads/{userId})
  • Reduce el impacto de huérfanos.

Orden de implementación sugerido

  1. Rate limiting en API (rápido + mayor impacto)
  2. Upload intents en DB (límite de activos + validación al crear/actualizar)
  3. Reducir TTL y mover a rutas temporales (TempUploads/...) + cleanup

Parámetros recomendados (config)

  • UploadUrlExpiresInSeconds (default: 120)
  • MaxActiveUploadIntentsPerUser (default: 5)
  • AllowedPictureExtensions / AllowedAudioExtensions
  • AllowedContentTypes (lista blanca)

Notas

  • Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos.