libro / blog / subir-imagenes-php

// imágenes

Subir y redimensionar imágenes en PHP de forma segura

Gustavo Arias9 min de lectura

TL;DR Nunca confíes en el nombre ni en el tipo que envía el navegador. Comprueba el error y el tamaño, detecta el tipo real con finfo, guarda el archivo con un nombre aleatorio fuera del alcance de PHP y, si puedes, vuelve a generar la imagen con GD: eso elimina cualquier contenido incrustado.

Fotos de perfil, imágenes de productos, adjuntos de un formulario: tarde o temprano tu aplicación aceptará archivos de los usuarios. Es una de las funciones más peligrosas que puedes programar, porque un archivo malicioso guardado en el lugar equivocado puede darle al atacante control total del servidor. En el libro PHP desde cero hasta experto aprendes a subir, validar y procesar imágenes; aquí tienes la versión segura paso a paso.

El formulario y $_FILES

<!-- enctype es obligatorio para enviar archivos -->
<form action="subir.php" method="post" enctype="multipart/form-data">
    <label for="foto">Foto de perfil (JPG, PNG o WebP, máx. 5 MB)</label>
    <input type="file" id="foto" name="foto" accept="image/jpeg,image/png,image/webp" required>
    <button type="submit">Subir</button>
</form>

El atributo accept solo filtra el selector de archivos para comodidad del usuario: no es una medida de seguridad. En PHP, el archivo llega en $_FILES['foto'] con cinco datos: name, type, tmp_name, error y size.

name y type los envía el navegador, así que el atacante los controla. Un archivo llamado gato.jpg con tipo image/jpeg puede ser un script PHP.

Validar: tamaño, tipo real y dimensiones

<?php
declare(strict_types=1);

const MAX_BYTES = 5 * 1024 * 1024;   // 5 MB
const TIPOS_PERMITIDOS = [            // tipo MIME real => extensión que usaremos
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/webp' => 'webp',
];

function subirImagen(array $archivo, string $carpeta): string
{
    // 1. Errores de la subida
    match ($archivo['error'] ?? null) {
        UPLOAD_ERR_OK => null,
        UPLOAD_ERR_NO_FILE => throw new RuntimeException('No se envió ningún archivo.'),
        UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => throw new RuntimeException('El archivo es demasiado grande.'),
        default => throw new RuntimeException('Error al subir el archivo.'),
    };

    // 2. Tamaño
    if ($archivo['size'] > MAX_BYTES) {
        throw new RuntimeException('El archivo supera los 5 MB.');
    }

    // 3. Tipo REAL, leyendo el contenido del archivo (no el que dice el navegador)
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($archivo['tmp_name']);
    $extension = TIPOS_PERMITIDOS[$mime] ?? throw new RuntimeException('Formato no permitido.');

    // 4. Que sea una imagen válida y de tamaño razonable
    $info = getimagesize($archivo['tmp_name']);
    if ($info === false || $info[0] > 6000 || $info[1] > 6000) {
        throw new RuntimeException('La imagen no es válida o es demasiado grande.');
    }

    // 5. Nombre aleatorio: nunca uses el nombre original
    $nombre = bin2hex(random_bytes(16)) . '.' . $extension;

    if (!move_uploaded_file($archivo['tmp_name'], $carpeta . '/' . $nombre)) {
        throw new RuntimeException('No se pudo guardar el archivo.');
    }
    return $nombre;
}

El paso 4 limita además las dimensiones: una imagen pequeña en bytes pero de 30.000 × 30.000 píxeles puede agotar la memoria del servidor al procesarla.

Guardar con un nombre seguro

<?php
declare(strict_types=1);
require_once __DIR__ . '/lib/subida.php';

try {
    $nombre = subirImagen($_FILES['foto'] ?? [], __DIR__ . '/uploads');
    // guarda $nombre en la base de datos, asociado al usuario
    $mensaje = 'Imagen subida correctamente.';
} catch (RuntimeException $e) {
    $mensaje = $e->getMessage();   // mensajes pensados para el usuario
}

Si la carpeta de subidas está dentro del directorio público, impide que el servidor ejecute código en ella:

# Apache: nunca ejecutar scripts en la carpeta de subidas
<FilesMatch "\.(php\d?|phtml|phar)$">
    Require all denied
</FilesMatch>
Options -Indexes

Mejor aún: guarda los archivos fuera de la carpeta pública y sírvelos mediante un script PHP que compruebe permisos.

Redimensionar con GD

Las fotos de un móvil pesan varios megas. Generar una versión más pequeña ahorra ancho de banda y, como beneficio extra de seguridad, volver a codificar la imagen descarta cualquier contenido incrustado en el archivo original.

<?php
declare(strict_types=1);

function crearMiniatura(string $origen, string $destino, int $anchoMax = 800): void
{
    [$ancho, $alto, $tipo] = getimagesize($origen);

    $imagen = match ($tipo) {
        IMAGETYPE_JPEG => imagecreatefromjpeg($origen),
        IMAGETYPE_PNG  => imagecreatefrompng($origen),
        IMAGETYPE_WEBP => imagecreatefromwebp($origen),
        default        => throw new RuntimeException('Formato no soportado.'),
    };

    // Mantener la proporción; no agrandar imágenes pequeñas
    $nuevoAncho = min($ancho, $anchoMax);
    $nuevoAlto  = (int) round($alto * $nuevoAncho / $ancho);

    $lienzo = imagecreatetruecolor($nuevoAncho, $nuevoAlto);
    imagealphablending($lienzo, false);   // conservar transparencias de PNG y WebP
    imagesavealpha($lienzo, true);
    imagecopyresampled($lienzo, $imagen, 0, 0, 0, 0, $nuevoAncho, $nuevoAlto, $ancho, $alto);

    imagewebp($lienzo, $destino, 82);   // WebP con calidad 82: buen equilibrio peso/calidad
}

// Uso, justo después de subirImagen():
// crearMiniatura(__DIR__ . "/uploads/$nombre", __DIR__ . "/uploads/thumb_$nombre.webp");
GD viene incluida en XAMPP, pero en algunos servidores hay que activarla. Comprueba con extension_loaded('gd'). Para tareas más avanzadas, la extensión Imagick es más potente.

Errores comunes

Para que los mensajes de error lleguen al usuario sin exponer detalles internos, revisa manejo de errores y excepciones en PHP.

Imágenes en tus aplicaciones reales

El libro PHP desde cero hasta experto enseña a subir, validar y procesar imágenes: redimensionar, guardar y mostrarlas en tus proyectos.

Comprar en Amazon