Flask

Flask

Tutorial Flask: Subir archivos en formularios Jinja en Flask

Aprende a configurar subidas de archivos en Flask, gestionando seguridad, rutas y validaciones esencialmente para el buen funcionamiento.

Aprende Flask GRATIS y certifícate

Configuración del sistema para uploads

Para habilitar la funcionalidad de subida de archivos en una aplicación Flask, es esencial configurar adecuadamente el sistema. Primero, es necesario definir una carpeta en el servidor donde se almacenarán los archivos subidos. Esta carpeta se especifica mediante la variable de configuración UPLOAD_FOLDER en la aplicación Flask:

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = '/ruta/a/la/carpeta/uploads'

Es recomendable utilizar rutas absolutas para evitar problemas al guardar los archivos. Además, es posible establecer un límite máximo para el tamaño de los archivos que se pueden subir utilizando la variable MAX_CONTENT_LENGTH. Esto ayuda a prevenir que usuarios malintencionados intenten subir archivos excesivamente grandes que puedan saturar el servidor:

app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16 megabytes

La variable MAX_CONTENT_LENGTH define el tamaño máximo en bytes. En este ejemplo, se establece un límite de 16 megabytes. Si un usuario intenta subir un archivo que excede este tamaño, Flask devolverá automáticamente un error 413 (Payload Too Large).

Es importante asegurarse de que la carpeta definida en UPLOAD_FOLDER exista y tenga los permisos adecuados. En sistemas Unix, puede crearse la carpeta y asignar los permisos correspondientes de la siguiente manera:

mkdir -p /ruta/a/la/carpeta/uploads
chmod 755 /ruta/a/la/carpeta/uploads

Para mejorar la seguridad al manejar los nombres de los archivos subidos, es aconsejable utilizar la función secure_filename proporcionada por Werkzeug. Esta función sanitiza el nombre del archivo para prevenir inyecciones o rutas maliciosas:

from werkzeug.utils import secure_filename

Durante el procesamiento de la subida, se utilizará secure_filename al guardar el archivo:

archivo = request.files['archivo']
nombre_archivo = secure_filename(archivo.filename)
ruta_completa = os.path.join(app.config['UPLOAD_FOLDER'], nombre_archivo)
archivo.save(ruta_completa)

Además, es recomendable definir un conjunto de extensiones permitidas para controlar qué tipos de archivos pueden ser subidos. Esto se logra creando una función que valida la extensión del archivo:

EXTENSIONES_PERMITIDAS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}

def extension_permitida(nombre_archivo):
    return '.' in nombre_archivo and \
           nombre_archivo.rsplit('.', 1)[1].lower() in EXTENSIONES_PERMITIDAS

Con esta función, se verifica si el archivo tiene una extensión válida antes de proceder a guardarlo, añadiendo una capa adicional de validación al proceso.

Por último, para que el servidor pueda recibir archivos desde un formulario HTML, es indispensable que el formulario tenga el atributo enctype establecido a multipart/form-data. De lo contrario, los datos del archivo no serán correctamente enviados:

<form method="post" enctype="multipart/form-data">
    <!-- Campos del formulario -->
</form>

Este ajuste en el formulario asegura que el navegador transmita correctamente los archivos al servidor Flask, permitiendo que la funcionalidad de subida opere sin inconvenientes.

Recepción y almacenamiento de archivos

Para manejar la recepción de archivos enviados desde un formulario HTML, Flask proporciona el objeto request.files, que contiene los archivos subidos por el usuario. Cada archivo puede ser accedido mediante el nombre del campo definido en el formulario.

Por ejemplo, si en la plantilla Jinja se tiene un formulario con un campo de tipo archivo:

<form method="post" enctype="multipart/form-data">
    <label for="archivo">Selecciona un archivo:</label>
    <input type="file" name="archivo" id="archivo">
    <input type="submit" value="Subir">
</form>

Es crucial que el formulario incluya el atributo enctype="multipart/form-data" para que los archivos se transmitan correctamente al servidor. Sin este atributo, el navegador no enviará los datos de los archivos y request.files estará vacío.

En la vista Flask correspondiente, se puede procesar la subida de la siguiente manera:

from flask import request, redirect, url_for, render_template
from werkzeug.utils import secure_filename
import os

@app.route('/subir', methods=['GET', 'POST'])
def subir_archivo():
    if request.method == 'POST':
        # Verificar si se ha enviado el archivo
        if 'archivo' not in request.files:
            return 'No se ha seleccionado ningún archivo'

        archivo = request.files['archivo']

        # Comprobar si el usuario no seleccionó un archivo
        if archivo.filename == '':
            return 'Nombre de archivo vacío'

        # Validar y guardar el archivo si es válido
        if archivo and extension_permitida(archivo.filename):
            nombre_seguro = secure_filename(archivo.filename)
            ruta_guardado = os.path.join(app.config['UPLOAD_FOLDER'], nombre_seguro)
            archivo.save(ruta_guardado)
            return 'Archivo subido exitosamente'

        else:
            return 'Extensión de archivo no permitida'

    # Renderizar el formulario en caso de método GET
    return render_template('subir.html')

En este código, se realizan varias comprobaciones para asegurar una gestión adecuada de la subida:

  • Se verifica que el campo 'archivo' existe en request.files.
  • Se comprueba que el nombre del archivo no esté vacío, lo que indicaría que no se ha seleccionado ningún archivo.
  • Se valida la extensión del archivo utilizando la función extension_permitida, previamente definida.

La función extension_permitida podría ser la siguiente:

EXTENSIONES_PERMITIDAS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}

def extension_permitida(nombre_archivo):
    return '.' in nombre_archivo and \
           nombre_archivo.rsplit('.', 1)[1].lower() in EXTENSIONES_PERMITIDAS

Al utilizar secure_filename, nos aseguramos de que el nombre del archivo sea seguro para almacenarlo en el servidor, evitando posibles ataques por parte de usuarios malintencionados.

Es recomendable generar un nombre único para cada archivo subido para evitar colisiones y posibles sobrescrituras. Una forma de hacerlo es mediante la generación de un identificador único universal (UUID):

import uuid

# ...

if archivo and extension_permitida(archivo.filename):
    nombre_extension = os.path.splitext(secure_filename(archivo.filename))[1]
    nombre_unico = str(uuid.uuid4()) + nombre_extension
    ruta_guardado = os.path.join(app.config['UPLOAD_FOLDER'], nombre_unico)
    archivo.save(ruta_guardado)

Con este enfoque, cada archivo subido tendrá un nombre único, preservando su extensión original, lo que facilita su gestión posterior.

Tras guardar el archivo, es útil proporcionar feedback al usuario. Esto se puede lograr mediante mensajes flash o redireccionando a una página que informe del éxito de la operación:

from flask import flash

# ...

if archivo and extension_permitida(archivo.filename):
    # ... código para guardar el archivo ...
    flash('Archivo subido correctamente')
    return redirect(url_for('subir_archivo'))

En la plantilla Jinja, se pueden mostrar estos mensajes:

{% with mensajes = get_flashed_messages() %}
  {% if mensajes %}
    <ul>
    {% for mensaje in mensajes %}
      <li>{{ mensaje }}</li>
    {% endfor %}
    </ul>
  {% endif %}
{% endwith %}

Es importante manejar excepciones que puedan ocurrir durante el proceso de guardado, como errores de entrada/salida o problemas de permisos. Se puede utilizar un bloque try-except para capturar y manejar estos errores:

try:
    archivo.save(ruta_guardado)
except Exception as e:
    # Registrar el error y notificar al usuario
    app.logger.error(f'Error al guardar el archivo: {e}')
    return 'Ocurrió un error al guardar el archivo'

Además de guardar el archivo en el sistema de archivos, puede ser necesario almacenar información adicional en una base de datos, como el nombre original, la ruta de almacenamiento, el tamaño o el tipo MIME. Esto facilita la recuperación y administración de los archivos subidos en la aplicación.

Por ejemplo, utilizando un modelo SQLAlchemy:

from models import Archivo
from datetime import datetime

# ...

if archivo and extension_permitida(archivo.filename):
    # ... código para guardar el archivo ...
    nuevo_archivo = Archivo(
        nombre_original=archivo.filename,
        nombre_guardado=nombre_unico,
        ruta=ruta_guardado,
        fecha_subida=datetime.now()
    )
    db.session.add(nuevo_archivo)
    db.session.commit()

De esta forma, se mantiene un registro de los archivos subidos, lo que es esencial para aplicaciones que requieren un histórico de archivos o funcionalidades avanzadas de gestión.

Validación y seguridad en la subida

Al implementar la funcionalidad de subida de archivos en Flask, es fundamental garantizar que el proceso sea seguro y que los archivos sean validados correctamente. Una gestión inadecuada puede exponer la aplicación a vulnerabilidades como la ejecución de código malicioso, ataques de denegación de servicio o filtraciones de información sensible. A continuación, se detallan prácticas recomendadas para mejorar la validación y la seguridad durante la subida de archivos.

En primer lugar, es esencial validar no solo la extensión del archivo sino también su tipo MIME. La extensión puede ser fácilmente manipulada por un usuario malintencionado, por lo que corroborar el tipo real del archivo añade una capa adicional de seguridad:

import mimetypes

def tipo_mime_permitido(nombre_archivo, tipo_mime_esperado):
    tipo_mime, _ = mimetypes.guess_type(nombre_archivo)
    return tipo_mime == tipo_mime_esperado

Este enfoque utiliza la biblioteca mimetypes para deducir el tipo MIME basado en el nombre del archivo. Sin embargo, para una verificación más robusta, es preferible analizar el contenido real del archivo:

from magic import from_buffer

def tipo_mime_verificado(archivo, tipo_mime_esperado):
    cabecera = archivo.read(1024)
    archivo.seek(0)  # Reiniciar el puntero del archivo
    tipo_mime = from_buffer(cabecera, mime=True)
    return tipo_mime == tipo_mime_esperado

Al utilizar la biblioteca python-magic, se inspecciona el contenido del archivo para determinar su tipo MIME real. Esto previene situaciones donde un archivo malicioso se disfraza con una extensión legítima.

Además de validar el tipo MIME, es recomendable implementar límites más estrictos en el tamaño máximo permitido para los archivos. Aunque ya se estableció MAX_CONTENT_LENGTH, se pueden definir límites específicos según el tipo de archivo:

LIMITES_TAMANO = {
    'image/png': 2 * 1024 * 1024,    # 2 MB para imágenes PNG
    'application/pdf': 5 * 1024 * 1024,  # 5 MB para PDF
}

def tamano_permitido(archivo, tipo_mime):
    archivo.seek(0, os.SEEK_END)
    tamaño = archivo.tell()
    archivo.seek(0)
    return tamaño <= LIMITES_TAMANO.get(tipo_mime, 0)

Este código verifica que el tamaño del archivo no exceda el límite establecido para su tipo MIME específico.

Es crucial protegerse contra ataques de travesía de directorios (directory traversal), donde un atacante intenta acceder o sobrescribir archivos sensibles del servidor. Aunque secure_filename ayuda a mitigar este riesgo, es buena práctica limitar aún más los nombres de archivo:

import re

def nombre_archivo_valido(nombre):
    patron = re.compile(r'^[a-zA-Z0-9_.-]+$')
    return patron.match(nombre) is not None

Esta función asegura que el nombre del archivo solo contenga caracteres alfanuméricos, guiones bajos, puntos y guiones, evitando caracteres especiales o secuencias peligrosas.

Otra consideración importante es almacenar los archivos en una ubicación segura fuera del directorio de la aplicación, preferiblemente fuera del alcance directo del servidor web. Esto evita que se puedan acceder directamente a los archivos subidos a través de una URL. Por ejemplo:

app.config['UPLOAD_FOLDER'] = '/var/www/misubidas'

Para servir estos archivos cuando sea necesario, se pueden crear vistas controladas que verifiquen permisos antes de enviar el archivo al cliente:

from flask import send_file

@app.route('/descargar/<nombre>')
def descargar_archivo(nombre):
    # Verificar que el usuario tiene permiso para acceder al archivo
    # ...

    ruta_archivo = os.path.join(app.config['UPLOAD_FOLDER'], nombre)
    return send_file(ruta_archivo, as_attachment=True)

Implementar medidas de autenticación y autorización es esencial para controlar quién puede subir y acceder a los archivos. Integrar Flask-Login y Flask-Principal permite gestionar sesiones, roles y permisos de manera eficiente.

Adicionalmente, para prevenir la ejecución de código malicioso, es recomendable deshabilitar la ejecución de scripts en la carpeta de subidas y establecer permisos restrictivos:

chmod 644 /var/www/misubidas/*  # Lectura y escritura para el propietario, lectura para otros

También es aconsejable limpiar y validar cualquier metadato incluido en los archivos, especialmente en imágenes o documentos que puedan contener información sensible o incrustar código.

Implementar una lista blanca de tipos y extensiones permitidas es más seguro que intentar bloquear tipos conocidos como peligrosos. Por ejemplo, en lugar de intentar rechazar .exe, se deben especificar explícitamente los tipos aceptados:

EXTENSIONES_PERMITIDAS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
TIPOS_MIME_PERMITIDOS = {'image/png', 'image/jpeg', 'application/pdf'}

En el procesamiento, se verifica que ambos, la extensión y el tipo MIME, estén permitidos.

Por último, es importante manejar y registrar adecuadamente los errores que puedan ocurrir durante la subida. No se deben exponer mensajes de error detallados al usuario final, ya que podrían revelar información sobre la infraestructura interna. En su lugar, se debe proporcionar un mensaje genérico y registrar el detalle en los archivos de log:

try:
    # Procesamiento y guardado del archivo
    # ...
except Exception as e:
    app.logger.error(f'Error al procesar el archivo: {e}')
    return 'Ocurrió un error al procesar el archivo'

Mantener los paquetes y dependencias actualizados es esencial para beneficiarse de las últimas correcciones de seguridad. Esto incluye actualizar Flask a su versión más reciente y revisar regularmente las dependencias utilizadas en el proyecto.

Ejemplo de uso con imágenes u otros formatos

En esta sección, se desarrollará un ejemplo práctico que permite a los usuarios subir imágenes y mostrarlas en una galería. Este ejemplo integra los conceptos previamente discutidos y demuestra cómo construir una funcionalidad completa de subida y visualización de archivos en Flask.

Paso 1: Estructura del proyecto

Organizar el proyecto en una estructura clara es fundamental. Se puede seguir la siguiente distribución de carpetas y archivos:

proyecto/
├── app.py
├── templates/
│   ├── base.html
│   ├── subir.html
│   └── galeria.html
├── static/
│   └── uploads/
  • app.py: archivo principal de la aplicación Flask.
  • templates/: carpeta que contiene las plantillas Jinja.
  • static/uploads/: carpeta donde se almacenarán las imágenes subidas.

Paso 2: Configuración de la aplicación

En app.py, se configura la aplicación y se establecen las rutas necesarias:

from flask import Flask, render_template, request, redirect, url_for
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)

# Configuraciones
app.config['UPLOAD_FOLDER'] = os.path.join('static', 'uploads')
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2 MB

# Extensiones permitidas
EXTENSIONES_PERMITIDAS = {'png', 'jpg', 'jpeg', 'gif'}

def extension_permitida(nombre_archivo):
    return '.' in nombre_archivo and \
           nombre_archivo.rsplit('.', 1)[1].lower() in EXTENSIONES_PERMITIDAS

Se define la carpeta de uploads dentro de static/uploads, y se establece un tamaño máximo para los archivos subidos. También se especifican las extensiones de imagen permitidas.

Paso 3: Rutas para subir y visualizar imágenes

Se crean las rutas para mostrar el formulario de subida, procesar la subida y mostrar la galería de imágenes.

@app.route('/')
def inicio():
    return redirect(url_for('galeria'))

@app.route('/subir', methods=['GET', 'POST'])
def subir_imagen():
    if request.method == 'POST':
        if 'archivo' not in request.files:
            return 'No se ha seleccionado ningún archivo', 400

        archivo = request.files['archivo']

        if archivo.filename == '':
            return 'Nombre de archivo vacío', 400

        if archivo and extension_permitida(archivo.filename):
            nombre_archivo = secure_filename(archivo.filename)
            ruta = os.path.join(app.config['UPLOAD_FOLDER'], nombre_archivo)
            archivo.save(ruta)
            return redirect(url_for('galeria'))
        else:
            return 'Extensión no permitida', 400

    return render_template('subir.html')

@app.route('/galeria')
def galeria():
    ruta_carpeta = app.config['UPLOAD_FOLDER']
    imagenes = os.listdir(ruta_carpeta)
    return render_template('galeria.html', imagenes=imagenes)

En la función subir_imagen, se manejan las solicitudes GET y POST para mostrar el formulario y procesar la subida, respetando las validaciones de seguridad establecidas. La función galeria obtiene la lista de imágenes en la carpeta de uploads y las pasa a la plantilla para su visualización.

Paso 4: Plantillas Jinja

Crear la plantilla base base.html con la estructura básica HTML:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>{% block titulo %}{% endblock %}</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.3.0/css/bootstrap.min.css">
</head>
<body>
    <div class="container mt-4">
        {% block contenido %}{% endblock %}
    </div>
</body>
</html>

La plantilla para el formulario subir.html extiende de base.html:

{% extends 'base.html' %}

{% block titulo %}Subir imagen{% endblock %}

{% block contenido %}
<h1>Subir una imagen</h1>
<form method="post" enctype="multipart/form-data">
    <div class="mb-3">
        <label for="archivo" class="form-label">Selecciona una imagen</label>
        <input type="file" class="form-control" id="archivo" name="archivo" accept="image/*" required>
    </div>
    <button type="submit" class="btn btn-primary">Subir</button>
</form>
<a href="{{ url_for('galeria') }}">Ver galería</a>
{% endblock %}

La plantilla para la galería galeria.html también extiende de base.html:

{% extends 'base.html' %}

{% block titulo %}Galería de imágenes{% endblock %}

{% block contenido %}
<h1>Galería de imágenes</h1>
<div class="row">
    {% for imagen in imagenes %}
    <div class="col-md-3 mb-4">
        <div class="card">
            <img src="{{ url_for('static', filename='uploads/' + imagen) }}" class="card-img-top" alt="{{ imagen }}">
        </div>
    </div>
    {% endfor %}
</div>
<a href="{{ url_for('subir_imagen') }}">Subir una nueva imagen</a>
{% endblock %}

En esta plantilla, se recorre la lista de imágenes y se muestran en un diseño de cuadrícula utilizando Bootstrap 5.

Paso 5: Ejecución de la aplicación

Para ejecutar la aplicación, se agrega la siguiente cláusula al final de app.py:

if __name__ == '__main__':
    app.run(debug=True)

Esto inicia el servidor de desarrollo de Flask en modo debug para facilitar las pruebas.

Paso 6: Pruebas y funcionamiento

Al visitar la ruta /subir, se mostrará el formulario para subir una imagen. Tras seleccionar una imagen y enviarla, el usuario es redirigido a la galería, donde se muestran todas las imágenes subidas.

Consideraciones adicionales

  • Es importante verificar que la carpeta static/uploads existe y tiene los permisos adecuados para escribir archivos.
  • Para evitar posibles conflictos de nombres de archivos, se puede modificar el nombre de los archivos subidos generando un identificador único:
import uuid

# Dentro de la función subir_imagen
if archivo and extension_permitida(archivo.filename):
    extension = os.path.splitext(archivo.filename)[1]
    nombre_archivo = f"{uuid.uuid4()}{extension}"
    ruta = os.path.join(app.config['UPLOAD_FOLDER'], nombre_archivo)
    archivo.save(ruta)
    return redirect(url_for('galeria'))

De esta manera, cada imagen subida tendrá un nombre único, evitando sobrescrituras accidentales.

  • Se pueden implementar mejoras adicionales como la validación del tipo MIME, restricciones de tamaño más específicas o la integración con una base de datos para mantener un registro de las imágenes subidas.
  • Para mayor seguridad, es recomendable servir los archivos estáticos a través de un servidor web como Nginx o Apache en un entorno de producción.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

20 % DE DESCUENTO

Plan mensual

19.00 /mes

15.20 € /mes

Precio normal mensual: 19 €
58 % DE DESCUENTO

Plan anual

10.00 /mes

8.00 € /mes

Ahorras 132 € al año
Precio normal anual: 120 €
Aprende Flask GRATIS online

Todas las lecciones de Flask

Accede a todas las lecciones de Flask y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Flask y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Configurar el sistema de archivos para subidas en Flask.
  • Definir rutas y permisos para almacenar archivos subidos.
  • Implementar límites de tamaño de archivo con MAX_CONTENT_LENGTH.
  • Utilizar secure_filename para proteger nombres de archivos.
  • Crear un sistema que permita subir y almacenar archivos de forma segura.
  • Validar extensiones de archivo permitidas.
  • Gestionar errores y excepciones durante el proceso de subida.