Protege la subida de archivos en tu API con números mágicos
Validar la subida de archivos es fundamental para la seguridad de tu API. Depender únicamente de las extensiones de archivo o de los tipos MIME puede dejar tu aplicación expuesta a ataques de suplantación. Un método más seguro consiste en revisar los números mágicos del archivo, también conocidos como firmas de archivo.
Por qué no basta con validar la extensión del archivo
Las extensiones de archivo y los tipos MIME se falsifican con facilidad. Un atacante puede renombrar
archivos maliciosos para que parezcan inofensivos y así eludir las comprobaciones básicas de
validación. Por ejemplo, un ejecutable malicioso podría renombrarse como
document.pdf y engañar a los sistemas que solo revisan la extensión.
La validación del tipo MIME es igual de poco fiable, porque los navegadores y los clientes pueden
establecer encabezados Content-Type arbitrarios. Por eso, la validación por números mágicos resulta
esencial para verificar archivos de forma robusta.
Comprende los números mágicos y las firmas de archivo
Los números mágicos son secuencias de bytes reconocibles, a menudo cerca del inicio de un archivo, que ayudan a identificar su formato probable. Son independientes del nombre del archivo y del tipo MIME proporcionado por el cliente, pero un atacante también puede falsificarlos. Que la firma coincida no demuestra que el archivo completo sea válido ni que sea seguro procesarlo. Considéralo una capa más de una política de subida.
Números mágicos habituales
A continuación tienes una lista breve, pero de uso frecuente. Para una visión completa, consulta «List of file signatures» en Wikipedia.
| Formato | Hex (offset 0) | Notas |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | Siempre 8 bytes |
| JPEG | FF D8 FF DBFF D8 FF E0FF D8 FF E1 | Cubre las variantes JFIF y EXIF |
| GIF | 47 49 46 38 37 61 (GIF87a)47 49 46 38 39 61 (GIF89a) | Seis bytes |
25 50 44 46 2D (%PDF-) | Cinco bytes | |
| ZIP | 50 4B 03 0450 4B 05 06 | DOCX, ODT y APK son contenedores ZIP |
| MP4 | 66 74 79 70 (offset 4) | Precedido por un campo de tamaño de cuatro bytes |
Implementa la validación por números mágicos en Node.js
El paquete file-type que se usa aquí es la versión 22 y requiere Node.js 22 o superior. Detecta
firmas a partir de un búfer o de un archivo. La
biblioteca es solo ESM, así que asegúrate de que el package.json de tu proyecto contenga "type": "module" o usa
archivos .mjs.
Instala las dependencias del ejemplo:
npm install file-type@22 express@4.22.2 multer@2.3.0
Inspecciona un archivo sin cargarlo por completo en memoria
import { fileTypeFromFile } from 'file-type'
/**
* Validate a file by magic number.
* @param {string} filePath Absolute or relative path to the file on disk
* @param {string[]} allowList Array of allowed MIME types
*/
export async function validateFileType(filePath, allowList = []) {
// Default allow-list: PNG, JPEG, and PDF
const allowedTypes = allowList.length ? allowList : ['image/png', 'image/jpeg', 'application/pdf']
const type = await fileTypeFromFile(filePath)
if (!type) throw new Error('Unknown or unsupported file type')
if (!allowedTypes.includes(type.mime)) {
throw new Error(`Disallowed file type: ${type.mime}`)
}
return { ...type, valid: true }
}
// Example
// (async () => {
// await validateFileType('uploads/avatar.png')
// })()
Valida subidas con límite de tamaño usando Express.js
Este endpoint almacena en búfer como máximo 5 MiB por archivo y luego comprueba su firma. De forma deliberada, no conserva la subida. No es una implementación de transmisión a disco; cuando uses almacenamiento en memoria, limita también la concurrencia y la frecuencia de solicitudes.
import express from 'express'
import multer from 'multer'
import { fileTypeFromBuffer } from 'file-type'
const app = express()
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024, files: 1, fields: 0 },
})
app.post('/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' })
const type = await fileTypeFromBuffer(req.file.buffer)
if (!type || !['image/png', 'image/jpeg'].includes(type.mime)) {
return res.status(400).json({ error: 'Invalid file type' })
}
res.json({ message: 'File validated', mime: type.mime, size: req.file.size })
} catch (err) {
res.status(400).json({ error: 'Unable to recognize this file' })
}
})
app.use((error, req, res, next) => {
if (res.headersSent) return next(error)
if (error instanceof multer.MulterError) {
return res.status(error.code === 'LIMIT_FILE_SIZE' ? 413 : 400).json({ error: 'Upload rejected' })
}
console.error('Upload processing failed')
res.status(500).json({ error: 'Unable to process the upload' })
})
app.listen(3000, () => console.log('API listening on :3000'))
Valida archivos en Python con python-magic
El ejemplo requiere Python 3.10 o posterior. python-magic es una envoltura ligera de la biblioteca C
libmagic, por lo que la instalación varía según la plataforma:
# Debian/Ubuntu
sudo apt-get install python3-magic libmagic1
# macOS (homebrew)
brew install libmagic
pip install python-magic
# Windows (pre-built binaries)
pip install python-magic-bin
from pathlib import Path
import magic
class FileValidator:
"""Validate MIME type using libmagic signatures."""
def __init__(self, allowed=None):
self.allowed = set(allowed or {
'image/png',
'image/jpeg',
'application/pdf',
})
self._mime = magic.Magic(mime=True)
def validate(self, file_path: str | Path) -> dict:
path = Path(file_path)
if not path.is_file():
raise FileNotFoundError(path)
mime_type = self._mime.from_file(str(path))
if mime_type not in self.allowed:
raise ValueError(f'Blocked MIME: {mime_type}')
return {
'mime': mime_type,
'size': path.stat().st_size,
'valid': True,
}
# Example
# validator = FileValidator()
# print(validator.validate('uploads/report.pdf'))
Gestiona casos límite y archivos políglotas
Algunos archivos se pueden interpretar como más de un formato. Buscar secuencias de bytes cortas como
MZ, <html o encabezados ZIP dentro de un archivo no es un detector fiable de archivos
políglotas: los datos binarios normales pueden contenerlas y los documentos válidos basados en ZIP
contienen encabezados ZIP repetidos. Las bibliotecas de firmas no son escáneres de malware. Después
de la identificación, usa un analizador o decodificador específico del formato y con mantenimiento
activo en un proceso aislado, aplica límites de recursos y analiza o reconstruye el contenido según
tu modelo de amenazas. No ejecutes las subidas ni sirvas contenido activo desde el origen de tu
aplicación.
Consejos de rendimiento para archivos grandes
- Prefiere las API de archivo o de flujo de la biblioteca. Algunos formatos requieren más que un prefijo de tamaño fijo; no existe una garantía universal de detección con 4.100 bytes.
- Procesa las subidas como flujos para evitar almacenar en memoria archivos enteros que ocupan gigabytes.
- Guarda en caché los arrays de tipos permitidos y las expresiones regulares, sobre todo en entornos serverless, donde los arranques en frío son costosos.
Combina la validación por números mágicos con otros controles
Los números mágicos sugieren el formato de un archivo; no demuestran su validez ni su seguridad. Refuerza tu pipeline de subida añadiendo capas de protección adicionales:
- Límites de bytes aplicados a las subidas, incluidas las solicitudes sin un
Content-Lengthfiable - Análisis de virus/malware (ClamAV o una API comercial)
- Limitación de frecuencia y autenticación en los endpoints de subida
- Encabezados Content-Security-Policy cuando sirvas archivos multimedia proporcionados por los usuarios
Prueba tu implementación
Unas cuantas pruebas unitarias (con Vitest, por ejemplo) ayudan a garantizar que las refactorizaciones futuras no rompan la validación. Prueba con archivos válidos, archivos mal formados y casos límite como archivos vacíos o archivos con extensiones incorrectas.
import { describe, expect, it } from 'vitest'
import { fileTypeFromBuffer } from 'file-type'
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j3ioAAAAASUVORK5CYII=',
'base64',
)
const empty = Buffer.alloc(0)
const jpeg = Buffer.from(
'ffd8ffe000104a46494600010100000100010000ffdb0043000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101ffc00011080001000103012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffda000c03010002110311003f00f2a900',
'hex',
) // A JPEG-signature fixture; this test does not validate complete image decoding.
describe('magic-number validation', () => {
it('detects PNG correctly', async () => {
const t = await fileTypeFromBuffer(png)
expect(t?.mime).toBe('image/png')
})
it('detects JPEG correctly', async () => {
const t = await fileTypeFromBuffer(jpeg)
expect(t?.mime).toBe('image/jpeg')
})
it('rejects empty buffers', async () => {
const t = await fileTypeFromBuffer(empty)
expect(t).toBeUndefined()
})
})
Resuelve problemas frecuentes
| Síntoma | Causa posible | Solución |
|---|---|---|
Error: Unknown or unsupported file type | file-type no puede encontrar ninguna firma coincidente | Pasa el archivo o el búfer completo, revisa los formatos compatibles y asegúrate de que el archivo no esté cifrado ni truncado |
Module not found: file-type | Uso de require() de CommonJS | Cambia a ESM o usa la importación dinámica import('file-type') |
ImportError: failed to find libmagic | Falta libmagic en el sistema operativo | Instálalo con tu gestor de paquetes o usa el wheel -bin en Windows |
Conclusión
Las comprobaciones de números mágicos ayudan a rechazar discrepancias de tipo evidentes antes del almacenamiento o el procesamiento. Combínalas con límites de tamaño aplicados, validación que tenga en cuenta el formato, análisis de malware y un manejo de errores robusto; ninguna comprobación de firma basta por sí sola para que una subida sea segura.
¿Necesitas una forma más sencilla de gestionar subidas a gran escala? Descubre nuestro servicio de subida de archivos en Transloadit.
