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");
extension_loaded('gd'). Para tareas más avanzadas, la extensión Imagick es más potente.
Errores comunes
- Validar por la extensión del nombre:
foto.php.jpgofoto.jpgcon contenido PHP pasan ese filtro. - Confiar en
$_FILES['foto']['type']: lo escribe el navegador. - Guardar con el nombre original: permite sobrescribir archivos de otros usuarios o
usar rutas como
../../index.php. - Olvidar los límites de
php.ini: si el archivo superaupload_max_filesizeopost_max_size,$_FILESllega con error o directamente vacío. - Mostrar SVG subidos por usuarios: un SVG puede contener JavaScript. Si no los necesitas, no los aceptes.
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