Verifica subidas web con Magika y una API de Python
Un archivo llamado photo.jpg podría contener un PDF. Para verificar el tipo de
un archivo subido, envía sus bytes a la API de Python de Magika y aplica
una lista de tipos permitidos a la etiqueta devuelta. Este tutorial crea una aplicación local de Flask
que sirve tanto la página de subida como su endpoint de clasificación, con mensajes que corresponden
a la selección actual.
El navegador envía el archivo completo a Python. La clasificación se realiza en ese servidor, incluso cuando ambos procesos se ejecutan en tu laptop. Que un tipo sea aceptado no demuestra que el archivo sea válido ni que esté libre de malware.
Decide qué puede indicarte la etiqueta
Las extensiones y los tipos MIME proporcionados por el cliente describen lo que un archivo dice ser. Los detectores de contenido, en cambio, inspeccionan los bytes, por lo que cambiar el nombre de un archivo no altera su entrada. Magika usa un modelo entrenado para distinguir formatos binarios y de texto; aun así, puede clasificar un archivo incorrectamente.
La API de Python expone
identify_bytes(), un indicador de éxito en result.ok y la predicción final en result.output.
Usa result.output.label para la lista de tipos permitidos. El modo predeterminado
HIGH_CONFIDENCE puede sustituir una predicción del modelo de baja confianza por una
etiqueta genérica, que este ejemplo rechaza junto con los demás tipos que no figuran en la lista.
Magika y los detectores basados en firmas responden a una pregunta sobre la identificación del formato. Ni esa respuesta ni una puntuación de confianza alta demuestran que un decodificador aceptará el archivo completo. Las limitaciones conocidas de Magika también excluyen la detección fiable de archivos políglotas: un archivo puede interpretarse como más de un formato.
Integra Magika en una aplicación de navegador
Usa Linux, Python 3.12 con venv y pip, y un navegador actual. El ejemplo
fija las versiones Magika 0.6.1 y Flask 3.1.0; la instalación requiere acceso a internet. Una vez
instalado, Magika carga localmente el modelo incluido, por lo que la clasificación no necesita un
servicio en la nube. Los comandos siguientes usan Bash.
1. Instala Magika
Pega esto en una terminal, en el directorio donde quieras crear un proyecto
magika-upload. El código se niega a reutilizar un directorio existente y se detiene
si falla algún paso de la configuración. Los paréntesis mantienen tu shell en su directorio original.
(
mkdir magika-upload &&
cd magika-upload &&
mkdir static &&
python3.12 -m venv .venv &&
.venv/bin/python -m pip install magika==0.6.1 flask==3.1.0
)
Continúa solo cuando la instalación se complete correctamente. Si falla, examina el error y vuelve a intentarlo en un directorio nuevo, o repara el entorno incompleto antes de continuar. No elimines un proyecto existente para volver a ejecutar la configuración.
2. Crea una API mínima de verificación (Flask)
Guarda esto como magika-upload/app.py. El modelo se carga una sola vez al iniciar la
aplicación. La ruta / sirve la página que crearás a continuación;
Flask también sirve automáticamente el directorio static.
from flask import Flask, jsonify, request, send_from_directory
from magika import Magika
from werkzeug.exceptions import RequestEntityTooLarge
app = Flask(__name__)
MAX_FILE_BYTES = 5 * 1024 * 1024
app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
ALLOWED_TYPES = {'pdf', 'jpeg', 'png'}
magika = Magika()
@app.get('/')
def index():
return send_from_directory(app.root_path, 'index.html')
@app.errorhandler(RequestEntityTooLarge)
def request_too_large(error):
return jsonify(error='Upload is too large'), 413
@app.post('/verify')
def verify_file():
if 'file' not in request.files:
return jsonify(error='No file provided'), 400
content = request.files['file'].read(MAX_FILE_BYTES + 1)
if not content:
return jsonify(error='File is empty'), 400
if len(content) > MAX_FILE_BYTES:
return jsonify(error='Upload is too large'), 413
result = magika.identify_bytes(content)
if not result.ok:
return jsonify(error='File analysis failed'), 500
if result.output.label not in ALLOWED_TYPES:
return jsonify(error='Only PDF, JPEG, and PNG files are allowed'), 415
return jsonify(
file_type=result.output.label,
mime_type=result.output.mime_type,
)
El límite por archivo es de 5 MiB. El límite independiente de 6 MiB por solicitud deja espacio para
los encabezados multipart y, a la vez, limita la solicitud completa. Como explica la
guía de subida de archivos de Flask, el análisis de los archivos
subidos puede usar almacenamiento temporal en disco. Esta aplicación no guarda los archivos
aceptados de forma permanente; lee como máximo 5 MiB más un byte en content
antes de clasificarlos.
3. Conecta la interfaz
Guarda esto como magika-upload/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Classify a file with Magika</title>
<script src="/static/verify.js" defer></script>
</head>
<body>
<h1>Classify a file with Magika</h1>
<p>PDF, JPEG, or PNG, up to 5 MiB. The file is sent to the local Python server.</p>
<form id="uploadForm" action="/verify" method="post" enctype="multipart/form-data">
<label for="fileInput">File to classify</label>
<input id="fileInput" name="file" type="file">
<button type="submit">Classify file</button>
</form>
<p id="result" role="status">Choose a file, then classify it.</p>
</body>
</html>
Guarda lo siguiente como magika-upload/static/verify.js. Al cambiar la selección, el resultado
anterior se borra de inmediato. Un nuevo envío sustituye la solicitud anterior, de modo que una
respuesta lenta no pueda reemplazar los mensajes correspondientes a una operación más reciente.
const form = document.getElementById('uploadForm')
const input = document.getElementById('fileInput')
const result = document.getElementById('result')
const errors = {
400: 'Choose a non-empty file.',
413: 'File exceeds the upload limit.',
415: 'Only PDF, JPEG, and PNG files are allowed.',
}
let requestId = 0
let activeRequest
input.addEventListener('change', () => {
requestId += 1
activeRequest?.abort()
result.textContent = 'Selection changed. Click Classify file to check it.'
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
const id = ++requestId
activeRequest?.abort()
const file = input.files[0]
if (!file) {
result.textContent = 'Choose a file, then classify it.'
return
}
const controller = new AbortController()
activeRequest = controller
const timeout = setTimeout(() => controller.abort(), 30_000)
const data = new FormData()
data.append('file', file)
result.textContent = `Classifying ${file.name}…`
try {
const response = await fetch('/verify', {
method: 'POST',
body: data,
signal: controller.signal,
})
if (id !== requestId) return
if (!response.ok) {
result.textContent = errors[response.status] ??
`Classification failed (HTTP ${response.status}).`
return
}
const analysis = await response.json()
if (id !== requestId) return
result.textContent = `${file.name}: ${analysis.file_type} (${analysis.mime_type}). ` +
'Accepted by the type allowlist.'
} catch {
if (id !== requestId) return
result.textContent = controller.signal.aborted
? 'Request timed out. Click Classify file to retry.'
: 'Could not reach or read the server. Check the terminal, then retry.'
} finally {
clearTimeout(timeout)
}
})
El ID de la solicitud protege tanto la respuesta como su cuerpo JSON, que se lee de forma asíncrona.
AbortController
deja de esperar las solicitudes sustituidas, y un temporizador de 30 segundos limita cada intento del
navegador. Cancelar la solicitud del navegador no garantiza que Python deje de procesar los bytes
que ya recibió.
Desde el mismo directorio padre que usaste para la configuración, inicia la aplicación:
(
cd magika-upload &&
.venv/bin/python -m flask --app app run --host 127.0.0.1 --port 5050
)
Espera el mensaje de inicio de Flask y luego abre http://127.0.0.1:5050/. Abre esta URL en
lugar del archivo HTML en disco: la página, el script y el endpoint /verify
deben compartir un origen. Selecciona un archivo con
File to classify y luego haz clic en
Classify file. Puedes volver a hacer clic en el botón
para reintentar con el mismo archivo. Detén el servidor con Ctrl+C cuando termines.
4. Compara los resultados con una lista de tipos permitidos
Prueba con un PNG pequeño y válido cuyo nombre hayas cambiado a photo.txt.
Deberías ver
photo.txt: png (image/png). Accepted by the type allowlist.
El nombre del archivo no autoriza la subida: solo lo hacen las etiquetas
pdf, jpeg y png del servidor.
Para los archivos JPEG, la etiqueta es jpeg, incluso cuando el nombre
termina en .jpg.
Después, prueba con un archivo de texto no vacío cuyo nombre hayas cambiado a
photo.png. Debería producir
Only PDF, JPEG, and PNG files are allowed.
Prueba también con un archivo vacío y con otro que supere ligeramente los 5 MiB. Estos se rechazan
antes de que se ejecute Magika. Mientras una solicitud esté pendiente, selecciona otro archivo:
la página debería borrar el resultado pendiente y esperar a que clasifiques la nueva selección.
Resuelve una solicitud fallida
- Falla la instalación o la carga del modelo: primero lee el error en la terminal. Confirma que la
instalación se completó en
.venvy que estás iniciando su ejecutable de Python. La página no puede cargarse hasta que la aplicación se inicie correctamente. - El puerto está ocupado: Flask se cierra con un error de dirección en uso. Elige otro puerto en el comando de inicio y abre la URL de ese puerto. Este es un servidor de desarrollo local, no una configuración de despliegue.
- El servidor rechaza la solicitud: HTTP 400 indica que falta el archivo o que está vacío, 413 indica un límite de tamaño y 415 indica una etiqueta fuera de la lista de tipos permitidos. Las solicitudes más grandes pueden causar un restablecimiento de la conexión en el servidor de desarrollo en lugar de una respuesta 413 legible.
- El navegador no puede leer una respuesta: revisa la terminal del servidor y la URL, y vuelve a intentarlo. Los errores HTTP, un servidor detenido y los tiempos de espera agotados no cuentan como aceptación. La página muestra mensajes de error fijos en lugar de mostrar una página de error del servidor o una traza de la pila.
Usa el resultado en un flujo de trabajo de subida
Si amplías esta aplicación para almacenar archivos, realiza la verificación de la lista de tipos permitidos y el almacenamiento sobre los mismos bytes recibidos. Una respuesta de clasificación exitosa no debe convertirse en permiso para una subida posterior sin verificar. El ejemplo devuelve únicamente una clasificación; no guarda ningún archivo subido ni crea una aprobación reutilizable.
Una etiqueta permitida puede ayudar a elegir un decodificador de imágenes, un analizador de documentos o un servicio de moderación para el procesamiento posterior. Esas operaciones siguen necesitando su propia validación y sus propios límites de recursos. El análisis de malware es una tarea aparte, y una predicción de formato nunca debe mostrarse como un veredicto de seguridad.
Para un flujo de trabajo de subida gestionado, consulta la referencia de verificación de archivos. Mantén la misma distinción en ambos diseños: reconocer un formato es una decisión dentro del manejo de una subida, no una prueba de que su contenido sea seguro.
