Inicio / Artículos

De la carga al URL: Almacenamiento y entrega seguros de archivos de usuario en Express

Aprenda dónde deben almacenar las aplicaciones Express los archivos subidos, cómo express.static asocia una carpeta con URLs y qué medidas de protección evitan que las subidas de los usuarios se conviertan en una brecha de seguridad.

Editor: REACTAPP.TOP7 min de lectura1589 palabras

Fuentes: Storing Uploaded Files and Serving Them in Express

Un usuario elige una foto de perfil, hace clic en subir y, un segundo después, aparece en su página. Detrás de esa sencilla interacción se esconden tres decisiones de diseño reales: dónde reside físicamente el archivo, cómo lo localiza nuevamente el servidor y cómo el navegador lo muestra finalmente. Esta guía aborda los tres aspectos en una aplicación Express, desde elegir la ubicación de almacenamiento hasta exponer los archivos a través de una URL, y finaliza con las precauciones que evitan que las subidas se conviertan en una superficie de ataque.

Donde almacenar los archivos subidos

Antes de que un archivo pueda ser servido nuevamente, debe escribirse en algún lugar. En la configuración más simple, se trata de una carpeta en el propio sistema de archivos del servidor, comúnmente denominada uploads y ubicada en la raíz del proyecto junto al código fuente, no dentro de él:

project-root/
  uploads/
    avatars/
      user-42.png
      user-91.png
    documents/
      invoice-108.pdf
  src/
    app.js

Grupar los archivos por finalidad, como avatars/ y documents/, rinde frutos rápidamente. Permite mantener la lista de directorios manejable a medida que aumenta su cantidad, facilita la aplicación de políticas de limpieza y respaldo por categoría, y, como verá en breve, genera URLs fáciles de predecir. Una sola carpeta plana que alberga todo tipo de archivos se vuelve difícil de gestionar mucho antes de convertirse en un problema de rendimiento.

En la mayoría de las aplicaciones, la base de datos solo almacena una referencia al archivo, como la ruta relativa avatars/user-42.png, no el archivo en sí ni una URL completa. Almacenar la ruta relativa permite cambiar posteriormente el dominio o pasar a un sistema de almacenamiento diferente sin tener que reescribir cada fila.

Disco local o almacenamiento de objetos externo

Mantener los archivos en el servidor de la aplicación

Escribir archivos directamente en el disco del servidor, como se muestra en la configuración anterior, no requiere infraestructura adicional. Es una opción adecuada para el desarrollo local, prototipos, herramientas internas y aplicaciones cuyo volumen de carga es pequeño y predecible.

Mover archivos a un servicio de almacenamiento

Los sistemas en producción suelen transferir los archivos a un almacén de objetos dedicado, como Amazon S3, Google Cloud Storage o un servicio similar, evitando así utilizar el disco del servidor de la aplicación.

Por qué es importante la elección

  • Los discos pueden ser pequeños y de corta duración. Muchas plataformas de hosting reconstruyen o reemplazan las instancias en cada despliegue, por lo que un archivo que solo existe en el disco local podría desaparecer la próxima vez que se envíe.
  • Las instancias múltiples no comparten un disco. Una vez que se escala horizontalmente, un archivo guardado en una instancia queda invisible para las demás. Un almacenamiento centralizado proporciona a cada instancia la misma vista.
  • Los almacenes de objetos están diseñados para esta tarea. La durabilidad, la replicación, las copias de seguridad y una entrega eficiente a gran escala son su propósito principal, mientras que un servidor de aplicaciones de uso general está optimizado para ejecutar tu código.
  • Nada de esto hace que el almacenamiento local sea incorrecto. Para proyectos de aprendizaje y aplicaciones realmente pequeñas, es un primer paso sensato, y las ideas mencionadas a continuación (rutas organizadas, URLs predecibles, validación cuidadosa) se mantienen sin cambios si más adelante migras a un almacenamiento externo. Mantener el código de almacenamiento detrás de un módulo pequeño con funciones como save y getUrl hace que esa migración sea mucho menos problemática.

    Permitir que express.static sirva la carpeta

    Una vez guardado el archivo, el navegador aún necesita una forma de obtenerlo. Express incluye un middleware para archivos estáticos precisamente con ese propósito:

    const express = require("express");
    const app = express();
    
    app.use("/uploads", express.static("uploads"));
    

    Esta línea monta el directorio uploads bajo el prefijo de URL /uploads. Para cualquier solicitud cuya ruta comience con ese prefijo, Express busca un archivo en la ubicación correspondiente dentro de la carpeta y, si existe, lo transmite mediante un tipo de contenido y encabezados de caché adecuados. No es necesario escribir un manejador por archivo.

    Eso representa un modelo mental diferente al de una ruta API ordinaria. En lugar de ejecutar lógica que carga datos y genera una respuesta, se publica toda una carpeta, basándose únicamente en la ruta. De esto se derivan dos observaciones prácticas:

    • El argumento de la carpeta se resuelve en relación con el directorio desde donde se inició el proceso de Node, no con el archivo que contiene el código. Utilizar una ruta absoluta, como la que se genera con path.join(__dirname, „..“, „uploads“), evita sorpresas cuando la aplicación se ejecuta desde un directorio de trabajo diferente.
    • Cualquier elemento que se coloque en esa carpeta se vuelve público. Solo monte el directorio que realmente debe estar expuesto.

    Cómo un archivo guardado se convierte en una URL

    Una vez configurada la distribución estática, la ruta en el disco y la ruta en la URL son idénticas:

    File on disk:     uploads/avatars/user-42.png
    Accessible at:    https://yourapp.com/uploads/avatars/user-42.png
    

    Gracias a esta correspondencia uno-a-uno, el frontend puede apuntar directamente al archivo, por ejemplo en una etiqueta de imagen, sin necesidad de un endpoint API dedicado para descargar los datos:

    <img src="https://yourapp.com/uploads/avatars/user-42.png" alt="User avatar" />
    

    Aquí es donde la estructura de carpetas de la primera sección resulta útil. Cualquier layout que elijas en el disco es exactamente el mismo que ven los clientes en las URLs; por lo tanto, una estructura ordenada y coherente te proporcionará enlaces igualmente ordenados y consistentes. También significa que la URL puede generarse a partir de la ruta relativa almacenada en la base de datos, lo que permite mantener el dominio y el prefijo en un único valor de configuración.

    Medidas de seguridad para las subidas de usuarios

    Las subidas son un objetivo frecuente de abuso, por lo que “la subida funciona” es solo el punto de partida. Cada uno de los siguientes pasos cierra una vulnerabilidad específica.

    Verifica qué es realmente el archivo

    Una extensión de nombre de archivo es simplemente texto elegido por el cliente y no demuestra nada. Inspeccione el tipo de contenido real, idealmente leyendo los primeros bytes del archivo en lugar de confiar en el tipo MIME declarado, y acepte únicamente lo que requiere la función. Un endpoint de avatar, por ejemplo, debería aceptar imágenes y nada más. Formatos como SVG y HTML merecen una precaución adicional porque los navegadores pueden ejecutar scripts incrustados en ellos.

    Límite de tamaño

    Establezca un tamaño máximo para cada carga. Sin límite, una sola solicitud de gran tamaño, ya sea accidental o intencionada, puede llenar el disco o saturar ancho de banda. Imponga el límite al analizar la solicitud, de modo que el servidor deje de leer temprano en lugar de aceptar primero todo el contenido.

    Genere sus propios nombres de archivo

    Nunca escriba un archivo con el nombre que proporcionó el cliente. Ese nombre puede contener caracteres extraños o secuencias de travesía de rutas como ../../config.js, destinadas a sortear el directorio de subida. Cree un nombre nuevo en el servidor, como un identificador aleatorio más una extensión verificada. Como efecto secundario, esto también impide que dos usuarios que suban ambos photo.jpg sobrescriban los archivos del otro.

    Mantenga el directorio de subida aislado

    Almacene las subidas en un directorio claramente separado del código fuente y la configuración, y asegúrese de que el middleware estático exponga únicamente ese directorio. Montar por error la raíz del proyecto publicaría los archivos de entorno y el código fuente para cualquiera que adivine la ruta.

    Piense en qué origen sirve los archivos

    Al servir contenido proporcionado por el usuario desde el mismo dominio que la aplicación principal, se pueden presentar problemas como el scripting entre sitios, ya que un archivo malicioso se ejecutaría con la origen de su sitio. Por esa razón, muchos sistemas en producción entregan los archivos subidos desde almacenamiento externo o un subdominio separado. El envío del encabezado X-Content-Type-Options: nosniff y, para descargas que no deben mostrarse de forma inline, del encabezado Content-Disposition: attachment, reduce aún más el riesgo.

    Preguntas frecuentes

    ¿Debería cada proyecto usar S3 o un servicio similar?

    No necesariamente. Para proyectos pequeños o de aprendizaje, el disco local es más sencillo y completamente adecuado. El almacenamiento externo se vuelve importante una vez que se despliega en una plataforma con un sistema de archivos efímero o se ejecutan varias instancias que deben acceder a los mismos archivos.

    ¿Necesita express.static un paquete adicional?

    No, está integrado en Express. El análisis de los archivos subidos desde un formulario multipart generalmente requiere una biblioteca como Multer, pero servir archivos que ya están guardados no lo necesita.

    ¿Es aceptable mantener el nombre original del archivo?

    Es más seguro no hacerlo. Un nombre generado por el servidor evita caracteres extraños, colisiones entre nombres idénticos y trucos de acceso ocultos en nombres de archivo manipulados. Si los usuarios necesitan ver el nombre original, guárdelo como metadatos en la base de datos.

    ¿Se puede restringir el acceso a los archivos subidos?

    Sí, pero no solo con express.static, ya que este entrega un archivo a cualquiera que conozca su URL. Los archivos privados requieren una ruta dedicada que verifique la autenticación y autorización antes de transmitir el archivo, o, en el caso del almacenamiento externo, enlaces de corta duración generados únicamente para usuarios autorizados.

    Conclusión

    Una funcionalidad sólida de carga responde en orden a cuatro preguntas. ¿Dónde se almacena el archivo: en el disco del servidor porque es sencillo, o en un almacenamiento de objetos porque sobrevive a los despliegues y permite escalabilidad? ¿Cómo lo localiza la aplicación posteriormente: mediante una estructura de carpetas organizada y una ruta relativa guardada en la base de datos? ¿Cómo lo obtendrá el navegador: a través de express.static mapeado a un prefijo de URL claro, o mediante una ruta protegida para archivos privados? ¿Y qué lo mantiene seguro: tipos verificados, límites de tamaño, nombres generados por el servidor y una ubicación de servicio con alcance restringido, idealmente separada? Una vez resueltas estas cuestiones, el resto de las funcionalidades es en su mayor parte cuestión de implementación técnica.

    Lecturas relacionadas