Subida de ficheros
PHP permite recibir archivos enviados desde un formulario HTML a través de la superglobal $_FILES. Leer el archivo es la parte fácil; la parte crítica es validarlo y almacenarlo de forma segura antes de moverlo a su destino definitivo.
El formulario HTML
Para que un formulario envíe ficheros, necesita dos cosas: el método POST y el atributo enctype="multipart/form-data". Sin enctype, el archivo no llega:
<form action="subir.php" method="post" enctype="multipart/form-data">
<label>Imagen: <input type="file" name="foto" accept="image/*"></label>
<button type="submit">Subir</button>
</form>
La superglobal $_FILES
Cuando llega un archivo, PHP lo guarda en un directorio temporal y rellena $_FILES["foto"] con cinco claves:
name: nombre original del archivo en el equipo del usuario.type: tipo MIME declarado por el navegador. No es fiable: el usuario puede poner lo que quiera.tmp_name: ruta al archivo temporal en el servidor.error: código de error (0 = sin error).size: tamaño en bytes.
<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
print_r($_FILES["foto"]);
// Array (
// [name] => gato.jpg
// [type] => image/jpeg <-- declarado por el navegador, no fiable
// [tmp_name] => /tmp/phpXk3mN2
// [error] => 0
// [size] => 84320
// )
}
Códigos de error
Comprueba siempre $_FILES["foto"]["error"] antes de procesar el archivo:
UPLOAD_ERR_OK(0): sin error.UPLOAD_ERR_INI_SIZE(1): el archivo superaupload_max_filesizeen php.ini.UPLOAD_ERR_FORM_SIZE(2): superaMAX_FILE_SIZEdeclarado en el formulario.UPLOAD_ERR_PARTIAL(3): el archivo se subió solo parcialmente.UPLOAD_ERR_NO_FILE(4): no se seleccionó ningún archivo.
Validación
Valida al menos tres cosas: que no hubo error, que el tipo MIME real es el esperado y que el tamaño es aceptable.
Para comprobar el tipo MIME real (no el declarado por el navegador) usa finfo:
<?php
function validar_imagen(array $archivo): string
{
if ($archivo["error"] !== UPLOAD_ERR_OK) {
return "Error en la subida: código " . $archivo["error"];
}
$tamano_max = 2 * 1024 * 1024; // 2 MB
if ($archivo["size"] > $tamano_max) {
return "El archivo supera el tamaño máximo de 2 MB.";
}
$tipos_permitidos = ["image/jpeg", "image/png", "image/webp"];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$tipo_real = $finfo->file($archivo["tmp_name"]);
if (!in_array($tipo_real, $tipos_permitidos, true)) {
return "Tipo de archivo no permitido: " . $tipo_real;
}
return ""; // sin errores
}
Mover el archivo al destino
Una vez validado, usa move_uploaded_file() para moverlo del directorio temporal al destino definitivo. Esta función verifica internamente que el archivo viene de una subida HTTP real, lo que añade una capa de seguridad frente a manipulaciones del tmp_name:
<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$archivo = $_FILES["foto"];
$error = validar_imagen($archivo);
if ($error !== "") {
echo htmlspecialchars($error);
exit;
}
// Generar nombre único para evitar colisiones y no exponer el nombre original
$extension = match($finfo->file($archivo["tmp_name"])) {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
};
$nombre_nuevo = bin2hex(random_bytes(16)) . "." . $extension;
$destino = __DIR__ . "/uploads/" . $nombre_nuevo;
if (!move_uploaded_file($archivo["tmp_name"], $destino)) {
echo "No se pudo guardar el archivo.";
exit;
}
echo "Archivo guardado como: " . htmlspecialchars($nombre_nuevo);
}
Nota: Nunca uses el nombre original del archivo ($_FILES["foto"]["name"]) como nombre en disco: puede contener caracteres peligrosos, rutas relativas (../../config.php) o sobreescribir archivos existentes. Genera siempre un nombre aleatorio.
Consideraciones de seguridad
- Directorio de uploads fuera del web root o sin ejecución de PHP. Si el directorio es accesible vía web y el servidor ejecuta PHP en él, un atacante puede subir un
.phpdisfrazado y ejecutarlo. Configura el servidor para no ejecutar PHP en esa carpeta, o guarda los archivos fuera del web root y sírvelos con un script dedicado. - Nunca confíes en
$_FILES["tipo"]: es el MIME que el navegador del usuario declara. Siempre verifica el tipo real confinfo. - Comprueba la extensión además del MIME si el servidor usa la extensión para decidir cómo procesar el archivo.
- Limita el tamaño tanto en PHP (
upload_max_filesizeen php.ini) como en tu código.
Recapitulación
- El formulario necesita
method="post"yenctype="multipart/form-data". $_FILES["campo"]contiene:name,type,tmp_name,error,size. Solotmp_name,errorysizeson fiables.- Valida el tipo MIME real con
finfo, no con$_FILES["tipo"]. - Mueve el archivo con
move_uploaded_file(). Usa un nombre aleatorio, nunca el original. - Guarda los archivos en un directorio donde PHP no se ejecute.
En la próxima lección: fechas y horas en PHP: cómo trabajar con timestamps, formatear fechas y calcular intervalos.