Volver al blog
Node.jsNext.jsSubida de ArchivosSeguridad de APIOWASP Top 10Seguridad WebSeguridad de Backend

Subida Segura de Archivos en Node.js y Next.js: Defensas, Ejemplos de Código y Checklist de Auditoría [2026]

Introducción

La subida de archivos es una de las funciones más peligrosas que puedes añadir a una aplicación web. Una foto de perfil, un documento adjunto a un ticket de soporte, una importación CSV de datos masivos — cada una de estas funciones aparentemente inocentes abre múltiples vectores de ataque a la vez.

En 2025, la base de datos CVE registró más de 180 vulnerabilidades relacionadas con la subida de archivos en aplicaciones web, desde exploits simples de path traversal hasta cadenas críticas de RCE. Para las startups que construyen aplicaciones Next.js con backends serverless, la superficie de ataque es incluso mayor porque el límite serverless a menudo elimina las protecciones tradicionales del sistema de archivos.

El problema central: la subida de archivos cruza todas las fronteras de seguridad de tu aplicación. Acepta datos binarios arbitrarios de clientes no confiables, los almacena en tu infraestructura (o en tu bucket de nube), los procesa a través de tu backend y potencialmente los sirve de vuelta a otros usuarios. Cada uno de estos pasos es un punto potencial de explotación.

En esta guía aprenderás:

  • Los seis vectores de ataque de subida de archivos más peligrosos para aplicaciones Node.js y Next.js
  • Código de validación y saneamiento de nivel producción que puedes copiar a tu proyecto
  • Cómo escanear subidas en busca de malware en arquitecturas serverless y basadas en servidor
  • Cómo almacenar archivos de forma segura (en disco, compatible con S3 y en base de datos)
  • Un checklist de auditoría completo para tu próxima revisión de código

Seis Vectores de Ataque que Enfrenta Toda Función de Subida de Archivos

Antes de escribir ningún código, esto es contra lo que nos defendemos:

1. Ejecución Arbitraria de Código (RCE)

Un atacante sube un archivo .js o .ejs que tu servidor interpreta si cae en un directorio accesible por web. En Next.js, esto se mitiga con la convención del directorio public/ y el aislamiento serverless, pero los backends Express personalizados o las rutas de API que escriben en el sistema de archivos siguen siendo vulnerables.

2. Path Traversal

El propio nombre de archivo es un arma. Un atacante nombra un archivo ../../etc/passwd o ../../../config/database.yml. Si tu código construye la ruta de guardado concatenando un directorio base con el nombre de archivo proporcionado por el usuario, estás escribiendo archivos en ubicaciones arbitrarias.

3. XSS Almacenado a través de Contenido Subido

Tus usuarios pueden subir SVGs, archivos HTML, PDFs con JavaScript incrustado o incluso una imagen cuidadosamente manipulada con payloads XSS en los datos EXIF. Si tu aplicación sirve estos archivos con un Content-Type permisivo, cada usuario que vea el archivo subido está en riesgo.

4. Denegación de Servicio (Agotamiento de Almacenamiento)

Un atacante sube un archivo de 10 GB (o 10.000 archivos que suman 10 GB) para llenar tu disco, tu bucket de S3 o tu base de datos. Sin límites, esto es trivial de automatizar.

5. Distribución de Malware

Tu función legítima de subida de archivos se convierte en un canal de distribución de malware. Los usuarios suben PDFs infectados a un repositorio de documentos compartido, y otros usuarios los descargan y abren confiando en ellos porque vienen de tu aplicación.

6. SSRF a través del Procesamiento de Archivos

Tu backend procesa los archivos subidos — redimensiona imágenes con Sharp, extrae texto con pdf-parse, genera miniaturas. Si la librería de procesamiento tiene vulnerabilidades de SSRF (por ejemplo, un parser de PDF que sigue enlaces externos), el atacante puede usar tu endpoint de subida como un relé de SSRF.


Resumen de Arquitectura: Un Pipeline Defensivo de Subida de Archivos

Un sistema seguro de subida de archivos procesa cada subida a través de seis puertas secuenciales:

Subida del Cliente → 1. Validación de Content-Type → 2. Comprobación de Extensión
→ 3. Inspección de Contenido → 4. Escaneo de Malware → 5. Almacenamiento Seguro
→ 6. Servicio Saneado

Implementemos cada puerta en Node.js y Next.js.

Puerta 1: Validación de Content-Type (Cliente + Servidor)

Nunca confíes en la cabecera Content-Type enviada por el navegador — es trivial de falsificar. Valida tanto en el cliente como en el servidor.

tsx
// Lado del cliente (Next.js Client Component — conveniencia, no seguridad)
"use client";

export function UploadForm() {
  const ALLOWED_TYPES = [
    "image/jpeg",
    "image/png",
    "image/webp",
    "application/pdf",
    "text/csv",
  ];

  function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (file && !ALLOWED_TYPES.includes(file.type)) {
      alert("Invalid file type");
      e.target.value = "";
    }
  }

  return (
    <input
      type="file"
      accept=".jpg,.jpeg,.png,.webp,.pdf,.csv"
      onChange={handleFileSelect}
    />
  );
}

La validación en el servidor debe re-comprobar porque la comprobación del lado del cliente es trivial de eludir. Aquí tienes una validación robusta para una ruta de API de Next.js:

ts
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";

const ALLOWED_TYPES = new Set([
  "image/jpeg",
  "image/png",
  "image/webp",
  "application/pdf",
  "text/csv",
]);

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get("file") as File | null;

  if (!file) {
    return NextResponse.json({ error: "No file provided" }, { status: 400 });
  }

  // Puerta 1: Validación de Content-Type en el servidor
  if (!ALLOWED_TYPES.has(file.type)) {
    return NextResponse.json(
      { error: `File type '${file.type}' is not allowed` },
      { status: 400 },
    );
  }

  // Puerta 2: Validación del tamaño de archivo
  if (file.size > MAX_FILE_SIZE) {
    const maxMb = MAX_FILE_SIZE / (1024 * 1024);
    return NextResponse.json(
      { error: `File exceeds maximum size of ${maxMb} MB` },
      { status: 413 },
    );
  }

  // Continuar con el procesamiento...
  const buffer = Buffer.from(await file.arrayBuffer());
  // ...
}

⚠️ Crítico: La propiedad file.type proviene de la cabecera Content-Type. Un atacante que use curl --form "file=@malware.exe;type=image/jpeg" eludirá esta comprobación. Por eso la Puerta 3 (inspección de contenido) es obligatoria.

Puerta 2: Lista Blanca de Extensiones (No Lista Negra)

Nunca construyas una lista negra de extensiones — los atacantes encontrarán una extensión que olvidaste. Usa siempre una lista blanca:

ts
const ALLOWED_EXTENSIONS = new Set([
  ".jpg", ".jpeg",
  ".png",
  ".webp",
  ".pdf",
  ".csv",
]);

function validateExtension(filename: string): boolean {
  // Obtener la extensión, en minúsculas, y manejar múltiples puntos de forma segura
  const dotIndex = filename.lastIndexOf(".");
  if (dotIndex === -1) return false;
  const ext = filename.slice(dotIndex).toLowerCase();
  return ALLOWED_EXTENSIONS.has(ext);
}

También elimina cualquier carácter de path traversal del nombre de archivo:

ts
function sanitizeFilename(filename: string): string {
  // Eliminar separadores de ruta y referencias al directorio padre
  return filename
    .replace(/[/\\]/g, "")      // Eliminar / y \
    .replace(/\.\./g, "")       // Eliminar ..
    .replace(/\0/g, "")         // Eliminar bytes nulos
    .trim();
}

Puerta 3: Inspección de Contenido (Magic Bytes)

Las cabeceras Content-Type mienten. Las extensiones de archivo mienten. Los magic bytes no. Cada formato de archivo tiene una firma fija al inicio de su contenido binario. Valida que los bytes reales coincidan con el tipo declarado:

ts
// lib/magic-bytes.ts
const MAGIC_BYTES: Record<string, Uint8Array[]> = {
  "image/jpeg": [new Uint8Array([0xFF, 0xD8, 0xFF])],
  "image/png": [new Uint8Array([0x89, 0x50, 0x4E, 0x47])],
  "image/webp": [new Uint8Array([0x52, 0x49, 0x46, 0x46])], // Cabecera "RIFF"
  "application/pdf": [new Uint8Array([0x25, 0x50, 0x44, 0x46])], // "%PDF"
};

export function validateMagicBytes(
  buffer: Buffer,
  mimeType: string,
): boolean {
  const expectedSignatures = MAGIC_BYTES[mimeType];
  if (!expectedSignatures) return false;

  return expectedSignatures.some((sig) => {
    if (buffer.length < sig.length) return false;
    return sig.every((byte, i) => buffer[i] === byte);
  });
}

Úsalo en tu handler de subida:

ts
// Dentro de la ruta de API
if (!validateMagicBytes(buffer, file.type)) {
  return NextResponse.json(
    { error: "File content does not match claimed type" },
    { status: 400 },
  );
}

Para PDFs, valida también la estructura del PDF más allá de la cabecera %PDF — los atacantes pueden anteponer %PDF a un ejecutable:

ts
// Validación de PDF más estricta: verificar el trailer del PDF
function validatePdfContent(buffer: Buffer): boolean {
  return (
    buffer.slice(0, 4).toString() === "%PDF" &&
    buffer.slice(-7).toString().trim() === "%%EOF"
  );
}

Puerta 4: Re-codificación de Imágenes (Defensa en Profundidad para Imágenes)

Para subidas de imágenes, la defensa individual más efectiva es la re-codificación: decodificar la imagen y re-codificarla con una librería de confianza. Esto elimina los datos EXIF, los metadatos y cualquier payload oculto en los datos de la imagen:

ts
// lib/reencode-image.ts
import sharp from "sharp";

export async function reencodeImage(
  buffer: Buffer,
  format: "jpeg" | "png" | "webp",
): Promise<Buffer> {
  return sharp(buffer)
    .ensureAlpha()              // Normalizar el canal alfa
    .toFormat(format, {
      quality: 85,              // Re-codificar con calidad fija (elimina EXIF)
      mozjpeg: true,            // Mejor compresión
    })
    .toBuffer();
}

Por qué funciona: Un payload XSS oculto en los metadatos de la imagen (EXIF, XMP) se elimina porque sharp decodifica la imagen a datos de píxeles en bruto y codifica un archivo completamente nuevo. Un GIF que se hace pasar por JPEG fallaría en el paso de decodificación. Un archivo poliglota que es a la vez un ZIP válido y un JPEG válido — sharp lo rechazará si el decodificador de imágenes no puede parsearlo, o lo saneará si puede.

Para PDFs, usa una herramienta como qpdf o pdf-lib para linealizar o re-serializar el documento, eliminando el JavaScript incrustado:

bash
# Saneamiento de PDF en el servidor
qpdf --linearize --remove-embedded-files input.pdf output.pdf

Puerta 5: Escaneo de Malware

Para aplicaciones de producción, escanea cada subida con un motor de detección de malware. Dos enfoques prácticos:

Opción A: ClamAV (self-hosted, gratuito)

bash
# Instalar ClamAV en tu servidor o imagen Docker
apt-get install clamav clamav-daemon
freshclam  # Actualizar definiciones de virus
ts
// lib/clamav.ts
import { spawn } from "child_process";

export async function scanForMalware(
  buffer: Buffer,
): Promise<{ clean: boolean; signature?: string }> {
  return new Promise((resolve, reject) => {
    const clamscan = spawn("clamscan", ["--stdin", "--no-summary"]);
    let output = "";

    clamscan.stdout.on("data", (data) => {
      output += data.toString();
    });

    clamscan.on("close", (code) => {
      // Código de salida 0 = limpio, 1 = infectado
      if (code === 0) resolve({ clean: true });
      else if (code === 1) {
        const match = output.match(/FOUND\s*$|:\s*(.+?)\s+FOUND/);
        resolve({ clean: false, signature: match?.[1] || "Unknown" });
      } else {
        reject(new Error(`clamscan exited with code ${code}`));
      }
    });

    clamscan.stdin.write(buffer);
    clamscan.stdin.end();
  });
}

Opción B: Compatible con serverless — escanea con AWS Lambda + capa de ClamAV

Para Vercel o Next.js serverless, ejecuta ClamAV en una función Lambda dedicada. Sube el archivo a S3, dispara una Lambda con una capa de ClamAV y rechaza si está infectado.

Puerta 6: Almacenamiento Seguro

Nunca sirvas archivos subidos por usuarios directamente desde el dominio de tu aplicación. Las razones:

  1. XSS de mismo origen: Un SVG subido con <script>alert(1)</script> se ejecuta en el origen de tu aplicación si se sirve desde tusitio.com/uploads/virus.svg
  2. Robo de cookies: El script del atacante se ejecuta en tu origen y puede leer las cookies
  3. Bypass de CSP: Tu CSP permite imágenes desde tu origen, así que el SVG se trata como un recurso válido

La solución: almacena en un dominio o subdominio separado.

ts
// lib/storage.ts — almacenamiento compatible con S3 (MinIO, AWS S3, R2)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import crypto from "node:crypto";

const s3 = new S3Client({
  region: process.env.S3_REGION!,
  endpoint: process.env.S3_ENDPOINT, // MinIO o endpoint personalizado
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!,
  },
});

export async function storeFile(buffer: Buffer, originalName: string) {
  // Generar un nombre de archivo aleatorio — NUNCA usar el nombre proporcionado por el usuario
  const ext = originalName.slice(originalName.lastIndexOf(".")).toLowerCase();
  const key = `${crypto.randomUUID()}${ext}`;

  await s3.send(
    new PutObjectCommand({
      Bucket: process.env.S3_BUCKET!,
      Key: key,
      Body: buffer,
      ContentType: "application/octet-stream", // Forzar descarga, nunca renderizar
      ContentDisposition: "attachment",         // Evitar que el navegador renderice
    }),
  );

  return { key };
}

Puntos clave:

  • Nombres de archivo aleatorios (UUID): previene ataques de enumeración (/uploads/user-1-profile-pic.jpg)
  • Content-Type: application/octet-stream + Content-Disposition: attachment: fuerza la descarga, nunca renderiza en el navegador
  • Dominio separado: sirve desde cdn.tustartup.com o tustartup.ams3.digitaloceanspaces.com
  • ACLs a nivel de objeto: mantén los objetos subidos privados, sírvelos mediante URLs firmadas o un proxy

Para almacenamiento en disco (servidor tradicional):

ts
import { writeFile, mkdir } from "node:fs/promises";
import path from "node:path";
import crypto from "node:crypto";

const UPLOAD_DIR = path.join(
  process.cwd(),
  "private",    // Fuera de public/ — NO lo sirve Next.js
  "uploads",
);

export async function storeFileOnDisk(buffer: Buffer) {
  const key = crypto.randomUUID();
  const dir = path.join(UPLOAD_DIR, key.slice(0, 2)); // Fragmentar por los 2 primeros caracteres
  const filepath = path.join(dir, key);

  await mkdir(dir, { recursive: true });
  await writeFile(filepath, buffer);

  return { key, filepath };
}

Sirve los archivos en disco a través de una ruta de API dedicada que establezca cabeceras seguras:

ts
// app/api/files/[key]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { readFile } from "node:fs/promises";
import path from "node:path";

const UPLOAD_DIR = path.join(process.cwd(), "private", "uploads");

export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ key: string }> },
) {
  const { key } = await params;

  // Validar el formato de la clave (sin path traversal)
  if (!/^[0-9a-f-]+\.[a-z]+$/.test(key)) {
    return NextResponse.json({ error: "Invalid key" }, { status: 400 });
  }

  const filepath = path.join(UPLOAD_DIR, key.slice(0, 2), key);

  try {
    const buffer = await readFile(filepath);
    return new NextResponse(buffer, {
      headers: {
        "Content-Type": "application/octet-stream",
        "Content-Disposition": "attachment",       // Forzar descarga
        "X-Content-Type-Options": "nosniff",        // Sin MIME sniffing
        "Cache-Control": "private, max-age=31536000, immutable",
      },
    });
  } catch {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }
}

Handler de Subida Completo: Todo Junto

Aquí está el handler de subida completo listo para producción que integra las seis puertas:

ts
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
import { validateMagicBytes, reencodeImage, scanForMalware, storeFile } from "@/lib";

const ALLOWED_TYPES = new Set([
  "image/jpeg", "image/png", "image/webp", "application/pdf", "text/csv",
]);
const ALLOWED_EXTENSIONS = new Set([
  ".jpg", ".jpeg", ".png", ".webp", ".pdf", ".csv",
]);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get("file") as File | null;

  if (!file) return NextResponse.json({ error: "No file" }, { status: 400 });

  // Puerta 1: Tamaño
  if (file.size > MAX_FILE_SIZE) {
    return NextResponse.json({ error: "File too large" }, { status: 413 });
  }

  // Puerta 2: Extensión
  const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
  if (!ALLOWED_EXTENSIONS.has(ext)) {
    return NextResponse.json({ error: "Extension not allowed" }, { status: 400 });
  }

  // Puerta 3: Content-Type (línea base)
  if (!ALLOWED_TYPES.has(file.type)) {
    return NextResponse.json({ error: "Type not allowed" }, { status: 400 });
  }

  const buffer = Buffer.from(await file.arrayBuffer());

  // Puerta 4: Magic bytes
  if (!validateMagicBytes(buffer, file.type)) {
    return NextResponse.json(
      { error: "Content does not match claimed type" },
      { status: 400 },
    );
  }

  // Puerta 5 (para imágenes): Re-codificar para eliminar metadatos
  let finalBuffer = buffer;
  if (file.type.startsWith("image/")) {
    const format = file.type === "image/png" ? "png" : "webp";
    try {
      finalBuffer = await reencodeImage(buffer, format as "jpeg" | "png" | "webp");
    } catch {
      return NextResponse.json({ error: "Invalid image" }, { status: 400 });
    }
  }

  // Puerta 6: Escaneo de malware
  if (process.env.CLAMAV_ENABLED === "true") {
    const result = await scanForMalware(finalBuffer);
    if (!result.clean) {
      return NextResponse.json(
        { error: "File rejected — malware detected", signature: result.signature },
        { status: 422 },
      );
    }
  }

  // Puerta 7: Almacenar de forma segura
  const { key } = await storeFile(finalBuffer, file.name);

  return NextResponse.json({ url: `/api/files/${key}` }, { status: 201 });
}

Específico de Next.js: Server Actions y Subida de Archivos

Si usas Server Actions de Next.js para la subida de archivos (App Router), se aplican los mismos principios. Sin embargo, las Server Actions tienen un límite de tamaño de cuerpo de 4.5 MB por defecto. Para archivos más grandes, usa el enfoque de ruta de API anterior:

ts
// app/upload/_actions.ts
"use server";

export async function uploadFile(formData: FormData) {
  const file = formData.get("file") as File;
  // ... mismo pipeline de validación que el anterior
}

Limitación: Las Server Actions en Next.js 16 no soportan subidas en streaming. Para archivos grandes (>50 MB), usa una ruta de API dedicada con streaming multipart o un flujo de URL firmada de S3 en el lado del cliente.


Previniendo Archivos Políglota

Un archivo poliglota es un único archivo válido en dos formatos a la vez — por ejemplo, un archivo ZIP válido que también es un JPEG válido. Estos pueden eludir las comprobaciones de magic bytes.

Estrategias de defensa en orden de efectividad:

  1. Re-codificar imágenes (Puerta 5 anterior): sharp fallará o saneará la mayoría de los políglotas porque decodifica el flujo de imagen en lugar de leerlo secuencialmente
  2. Validar la integridad estructural: Para PDFs, valida el trailer %%EOF. Para imágenes, confirma que las dimensiones son razonables (por ejemplo, sharp lanzará una excepción si no puede determinar las dimensiones de la imagen)
  3. Ejecutar validadores de formato separados: Usa pdfinfo para PDFs, exiftool para imágenes, el comando file (libmagic) para una identificación de formato exhaustiva
ts
// Ejecutar libmagic como segunda opinión
import { file } from "tmp-promise";
import { exec } from "child_process";
import { promisify } from "util";
import { writeFile } from "fs/promises";

const execAsync = promisify(exec);

export async function identifyFileType(buffer: Buffer): Promise<string> {
  const { path } = await file({ postfix: ".tmp" });
  await writeFile(path, buffer);
  const { stdout } = await execAsync(`file --brief --mime-type "${path}"`);
  return stdout.trim();
}

Checklist de Auditoría para tu Próxima Revisión de Código

Copia este checklist en tu proceso de revisión de código para cualquier función que acepte subidas de archivos:

| # | Comprobación | Apto/No apto | |---|-------|-----------| | 1 | Lista blanca de extensiones (no lista negra) aplicada en el servidor | □ | | 2 | Content-Type validado en el servidor (no solo en el cliente) | □ | | 3 | Verificación de magic bytes contra el contenido real del archivo | □ | | 4 | Límite de tamaño de archivo aplicado antes de leer el archivo completo en memoria | □ | | 5 | Re-codificación de imágenes con Sharp (o similar) para eliminar metadatos | □ | | 6 | Escaneo de malware con ClamAV o un servicio en la nube | □ | | 7 | Nombres de archivo aleatorios — sin nombres proporcionados por el usuario en disco | □ | | 8 | Cabeceras de servicio segurasContent-Type: octet-stream, Content-Disposition: attachment, X-Content-Type-Options: nosniff | □ | | 9 | Dominio o subdominio separado para servir las subidas | □ | | 10 | Protección contra path traversal — eliminar ../, ..\\, bytes nulos de los nombres de archivo | □ | | 11 | Limitación de velocidad (rate limiting) en los endpoints de subida para prevenir el agotamiento de almacenamiento | □ | | 12 | Autenticación y autorización — verificar que quien sube tiene permiso | □ | | 13 | Defensas contra archivos políglota — re-codificar o ejecutar múltiples validadores de formato | □ |


Probando la Seguridad de tu Subida

Aquí tienes un script de prueba práctico que puedes ejecutar contra tu propia aplicación:

bash
#!/bin/bash
# test-upload-security.sh
# Probar cada vector de ataque contra tu endpoint de subida

URL="${1:-http://localhost:3000/api/upload}"

echo "=== Testing Upload Security ==="

# Test 1: Real file should succeed
echo -n "Test 1 (valid JPEG): "
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@test-data/valid.jpg" "$URL"
echo

# Test 2: Trojan — rename .exe to .jpg
echo -n "Test 2 (exe masked as jpg): "
echo "MZ" > /tmp/test.exe
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@/tmp/test.exe;type=image/jpeg" "$URL"
echo

# Test 3: Path traversal in filename
echo -n "Test 3 (path traversal): "
echo "test" > /tmp/payload.txt
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@/tmp/payload.txt;filename=../../../etc/passwd.txt" "$URL"
echo

# Test 4: SVG with XSS payload
echo -n "Test 4 (XSS in SVG): "
printf '<svg onload="alert(1)">' > /tmp/xss.svg
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@/tmp/xss.svg" "$URL"
echo

# Test 5: Huge file
echo -n "Test 5 (100MB file): "
dd if=/dev/zero bs=1M count=100 of=/tmp/huge.bin 2>/dev/null
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@/tmp/huge.bin" "$URL"
echo

# Test 6: Null byte injection
echo -n "Test 6 (null byte): "
echo "test" > /tmp/evil.txt
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@/tmp/evil.txt;filename=malicious.php%00.jpg" "$URL"
echo

# Test 7: Double extension
echo -n "Test 7 (double extension): "
echo "test" > /tmp/evil.txt
curl -s -o /dev/null -w "%{http_code}" \
  -F "file=@/tmp/evil.txt;filename=readme.txt.js.php.jpg" "$URL"
echo

Conclusión

La subida de archivos no es inherentemente peligrosa — son las suposiciones que hacemos sobre los datos subidos las que crean vulnerabilidades. Cada subida debe tratarse como entrada binaria no confiable en cada etapa del pipeline: validación, procesamiento, almacenamiento y servicio.

El pipeline de seis puertas descrito aquí — validación de contenido, magic bytes, re-codificación de imágenes, escaneo de malware, almacenamiento seguro y servicio seguro — proporciona defensa en profundidad. Ninguna puerta es perfecta por sí sola, pero juntas hacen que la explotación sea significativamente más difícil.

Añade el checklist de auditoría a tu proceso de revisión de código. Ejecuta el script de prueba antes de cada despliegue que toque la subida de archivos. Y si necesitas una revisión más profunda de la seguridad de subida de tu aplicación, contáctanos para una auditoría de seguridad integral.

Comparte este artículo:TwitterLinkedIn
JS

JS Security Audit

Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.