Apuntes sobre Php

PHP: Porque a veces necesitas un poco de caos en tu vida.

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 supera upload_max_filesize en php.ini.
  • UPLOAD_ERR_FORM_SIZE (2): supera MAX_FILE_SIZE declarado 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 .php disfrazado 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 con finfo.
  • 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_filesize en php.ini) como en tu código.

Recapitulación

  • El formulario necesita method="post" y enctype="multipart/form-data".
  • $_FILES["campo"] contiene: name, type, tmp_name, error, size. Solo tmp_name, error y size son 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.

TOP