Técnico 18 de abr. de 2026 · 7 min de lectura

Cómo moderar las imágenes que suben los usuarios antes de guardarlas en S3

Si moderas después de subir, el contenido peligroso ya existe en tu infraestructura. Así se invierte el orden.

Eduardo Lázaro
Eduardo Lázaro
Fundador de ToxicFilter
Cómo moderar las imágenes que suben los usuarios antes de guardarlas en S3

El tutorial de AWS de siempre te enseña a subir las imágenes directamente a S3 y procesarlas después. Para moderar contenido multimedia, ese orden está justo al revés. Para cuando analizas la imagen, ya está en tu infraestructura, bajo tu política de bucket, con tus reglas de conservación, y para ciertas categorías de contenido eso puede ser un problema legal de verdad.

Por qué "subir primero, moderar después" es un problema

  • CSAM. En la mayoría de jurisdicciones, poseer este contenido es delito sea cual sea la intención. "Íbamos a analizarlo" no es una defensa. El tiempo que pasa entre la subida y la detección es un agujero de cumplimiento normativo.
  • Imágenes íntimas no consentidas (NCII). El mismo tipo de problema, el mismo riesgo legal.
  • Seguridad de marca. Aunque el contenido solo sea desagradable (no ilegal), que exista en tu almacenamiento, aunque sea unos minutos, deja rastro en los registros de auditoría y en las copias de seguridad, y puede salir en un proceso judicial.
  • Superficie de ataque. Tener contenido malicioso en tu bucket abre una vía de ataque. Y que un bucket acabe mal configurado pasa. No guardes lo que no has aprobado.

Patrón 1: subida prefirmada con una pasarela de moderación

El enfoque más limpio: el cliente sube a una pasarela (Lambda + API Gateway, o un servidor ligero), que modera en memoria y solo reenvía a S3 si se aprueba.

// Lambda pseudo-code
import ToxicFilter from 'toxicfilter-sdk';

const tf = new ToxicFilter(process.env.TOXICFILTER_KEY);

export const handler = async (event) => {
    const imageBuffer = Buffer.from(event.body, 'base64');

    // Moderation in memory, never touches storage
    const verdict = await tf.imageData(imageBuffer);

    if (!verdict.allowed) {
        return {
            statusCode: 422,
            body: JSON.stringify({
                error: 'Content not allowed',
                reason: verdict.reason
            })
        };
    }

    // Approved, now write to S3
    const key = `uploads/${uuid()}.jpg`;
    await s3.putObject({
        Bucket: 'prod-user-uploads',
        Key: key,
        Body: imageBuffer,
        ContentType: 'image/jpeg'
    });

    return { statusCode: 200, body: JSON.stringify({ key }) };
};

Ventajas: el contenido nunca llega a S3 si no se aprueba. Desde el punto de vista legal, no hay nada que explicar.

Inconvenientes: duplica el coste del ancho de banda de entrada. Lambda tiene límites de tamaño de payload (6MB síncrono, 256KB asíncrono); para ficheros más grandes necesitas un montaje con streaming.

Patrón 2: bucket de cuarentena

Para ficheros grandes, donde pasarlos en streaming por una Lambda no es práctico, usa un patrón de dos buckets:

  1. El cliente sube a uploads-quarantine (un bucket con acceso estricto, sin lectura pública y con TTL corto).
  2. Un evento de S3 dispara un worker de moderación.
  3. Si se aprueba, el worker lo copia a uploads-approved (el bucket del que lee tu app).
  4. El bucket de cuarentena tiene una regla de ciclo de vida que borra los objetos a las 24 horas pase lo que pase.

Lo importante:

  • El bucket aprobado es el único que consulta tu app. Las URL de lectura prefirmadas solo salen de ahí.
  • El bucket de cuarentena no tiene acceso público. Nunca. Ni por comodidad ni para depurar.
  • El contenido rechazado se borra en el momento del rechazo, sin esperar a la regla de ciclo de vida.
  • Para el contenido ilegal (CSAM), tienes una vía de denuncia obligatoria ante el NCMEC o su equivalente. El proveedor de moderación debería ofrecer un flujo documentado para ello.

Patrón 3: comprobación previa por hash (barata y rápida)

Antes de lanzar la clasificación completa de la imagen, compara su hash con bases de datos de contenido malicioso conocido. PhotoDNA, PDQ y hashes perceptuales parecidos identifican en menos de 10ms contenido que ya se ha marcado antes. Es especialmente eficaz con CSAM e imágenes terroristas conocidas.

El orden de operaciones que recomendamos:

  1. Comprobación de hash (~10ms): rechaza al instante el material malicioso conocido.
  2. Clasificador rápido (~50ms): detecta la mayor parte del contenido nuevo.
  3. Clasificador profundo (~200ms): para todo lo que dejó dudas al clasificador rápido.
  4. Revisión humana: para la estrecha franja de contenido en la que el modelo profundo seguía con dudas.

¿Y el EXIF y la limpieza de metadatos?

Moderar y limpiar metadatos son cosas distintas, pero a menudo van juntas. En cualquier caso, lo normal es que quieras quitar las coordenadas GPS y el EXIF personal de las subidas de los usuarios antes de guardarlas, por privacidad, no por moderación. Hazlo en el mismo paso de la pasarela. Nosotros preferimos quitar el EXIF, recodificar a un formato canónico y después guardar.

Donde fallan casi todos los equipos

Moderan la imagen que guardan. No moderan las miniaturas, las variantes redimensionadas ni las versiones recortadas. Si tu pipeline genera 5 copias redimensionadas de cada subida, o las moderas todas (un desperdicio) o confías en que, si el original está aprobado, todas sus versiones derivadas son seguras (suele ser así, pero no siempre: un recorte agresivo puede quedarse justo en una zona peligrosa).

Lo más seguro: modera el original antes de generar ninguna versión derivada. Si el original se aprueba, genera las variantes. Si se rechaza, nunca se crea ninguna. Sencillo y auditable.

Pruébalo con tu tráfico real

Permitir, revisar o bloquear, y el motivo explicado. En el plan gratuito: 2.000 créditos al mes, sin tarjeta. Una comprobación cuesta 1 crédito, unos 8 si la lee el modelo y unos 10 por imagen.