Cómo hacer upload de archivos en PHP con buenas prácticas de seguridad

23/08/2024 | PHP, Seguridad | 0 comentarios

Guía paso a paso para subir archivos en PHP con validación de tipo, tamaño y seguridad para evitar riesgos.

Descargar archivos


El upload o subida de archivos es una funcionalidad común en muchas aplicaciones web, pero si no se maneja correctamente, puede convertirse en un problema de seguridad. En este artículo verémos cómo implementar el upload de archivos en PHP de manera segura.

Formulario HTML

Primero, necesitamos un formulario HTML que permita al usuario seleccionar un archivo y enviarlo al servidor:


<form action="upload.php" method="post" enctype="multipart/form-data">
    <label for="photo">Selecciona un archivo:</label>
    <input type="file" name="photo" id="photo" required>
    <button type="submit">Subir Archivo</button>
</form>

El atributo enctype="multipart/form-data" indica al navegador que debe codificar el formulario de una manera que soporte el envío de archivos.

Recibir archivos en PHP

Cuando se sube un archivo con PHP, este archivo se guarda en una carpeta temporal y los datos están disponibles en la variable global $_FILES. Se usa los datos de esta variable para copiar/mover el archivo a una carpeta final antes que sea eliminado.

La variable $_FILES contiene los siguientes datos:

ParámetroDescripción
$_FILES['userfile']['name']El nombre original del archivo subido.
$_FILES['userfile']['type']El tipo MIME del archivo.
$_FILES['userfile']['tmp_name']El nombre temporal del archivo en el servidor.
$_FILES['userfile']['size']El tamaño del archivo en bytes.
$_FILES['userfile']['error']El código de error asociado con la subida del archivo.

Upload básico de archivos

Ahora que sabemos que la variable $_FILES contiene los datos del archivo subido, podemos mover el archivo a la carpeta final utilizando la función move_uploaded_file.


<?php
// file: upload.php

$uploadDir = "files/";
$uploadFile = $uploadDir . basename($_FILES['photo']['name']);

if (move_uploaded_file($_FILES['photo']['tmp_name'], $uploadFile)) {
    echo "File was successfully uploaded.\n";
} else {
    echo "Error on upload file!\n";
    print_r($_FILES);
}

Validar los archivos subidos

Vamos a mejorar nuestros script validando el tipo de archivo subido, el tamaño del archivo antes de guardarlo. Usaremos mime_content_type para obtener el tipo MIME del archivo y pathinfo para obtener la extensión del archivo. Como ejemplo sólo aceptaremos imágenes en formato JPG, PNG y GIF.


<?php
// file: upload.php

$uploadDir = "files/";
$uploadFile = $uploadDir . basename($_FILES['photo']['name']);
$filesize = $_FILES['photo']['size'];
$filetype = strtolower(pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION));
$mimetype = mime_content_type($_FILES['photo']['tmp_name']);

// Validamos tamaño
if ($filesize == 0) {
    die('Error: Archivo incompleto o dañado.');
}

// Validamos la extensión
if (!in_array($filetype, array('jpg', 'png', 'gif'))) {
    die('Error: Tipo de archivo no permitido.');
}

// Validamos el mime type
if (!in_array($mimetype, array('image/png', 'image/jpeg', 'image/gif'))) {
    die('Error: Tipo de archivo no permitido.');
}

// Validamos si el upload tiene error
if ($_FILES['photo']["error"] !== UPLOAD_ERR_OK) {
    die("Error: " . $_FILES['photo']["error"]);
}

if (move_uploaded_file($_FILES['photo']['tmp_name'], $uploadFile)) {
    echo "File was successfully uploaded.\n";
} else {
    echo "Error on upload file!\n";
    print_r($_FILES);
}

Generalizando el Upload

Ahora que ya sabemos como validar los archivos recibidos en PHP podemos generalizar el upload y crear un clase PHP que se encargue de validar y procesar el upload de archivos.


<?php
// file: class/FileUpload.php

class FileUpload
{
    private string $folder;
    private int $maxSize;
    private array $TYPES;
    private array $MIMES;
    private array $error;

    public function __construct() {
        $this->folder = "uploads";
        $this->maxSize = 0;
        $this->TYPES = [];
        $this->error = [];
    }

    public function setFolderDestination(string $folder)
    {
        $this->folder = $folder;
    }

    public function setMaxSize(int $maxSize) {
        $this->maxSize = $maxSize;
    }

    public function setTypesAllowed(array $types) {
        $this->TYPES = $types;
        $this->checkAllowedMimes();
    }

    public function save(string $name, string $folder = '') {
        $file = $_FILES[$name];
        $this->folder = ($folder != '') ? $folder : $this->folder;

        $name = basename($file['name']);                                    // file name
        $size = $file['size'];                                              // file size
        $type = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));    // file type
        $mime = mime_content_type($file['tmp_name']);                       // mime type

        // Check upload error
        if ($file["error"] !== UPLOAD_ERR_OK) {
            $this->error[] = "Error: " . $file["error"];
            return false;
        }

        // Check file type
        if (!in_array($type, $this->TYPES)) {
            $this->error[] = "Error (" . $name . "): Tipo de archivo no permitido (File type).";
            return false;
        }

        // Check mime type
        if (!in_array($mime, $this->MIMES)) {
            $this->error[] = "Error (" . $name . "): Tipo de archivo no permitido (MIME type).";
            return false;
        }

        // Check file size
        if ($this->maxSize > 0 && $this->maxSize < $size) {
            $this->error[] = "Error (" . $name . "): El archivo es demasiado grande.";
            return false;
        }

        // Move uploaded file to target directory
        $newFile =  uniqid() . '-' . $name;
        if (move_uploaded_file($file['tmp_name'], $this->folder . '/' . $newFile)) {
            return $newFile;
        } else {
            $this->error[] = "Error (" . $name . "): Hubo un problema al subir el archivo.";
            return false;
        }
    }

    public function hasError() {
        return (sizeof($this->error) > 0);
    }

    public function getErrorMessage() {
        return implode('
', $this->error); } private function checkAllowedMimes() { $mimeTypes = array( // images 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'svg' => 'image/svg+xml', // documents 'pdf' => 'application/pdf', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // files 'zip' => 'application/zip', 'rar' => 'application/x-rar-compressed', 'txt' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', 'xml' => 'application/xml', 'css' => 'text/css', 'js' => 'application/javascript', 'json' => 'application/json', ); $allowedMimes = array(); foreach ($this->TYPES as $type) { $allowedMimes[] = $mimeTypes[$type]; } $this->MIMES = $allowedMimes; } }

Esta clase ofrece 6 métodos:

  • setFolderDestination: Define la carpeta donde se guardarán los archivos.
  • setMaxSize: Tamaño máximo en bytes del archivo que se puede aceptar.
  • setTypesAllowed: Array con las extensiones de archivos que se aceptan.
  • save: Método para guardar el archivos subido, recibe el nombre del archivo.
  • hasError: Devuelve verdadero si existe un error en el upload.
  • getErrorMessage: Si el upload ha fallado devuelve el mensaje de error.

Luego podemos convertir el anterior script para procesar upload de archivos a:


<?php
require 'class/FileUpload.php';

$upload = new FileUpload();
$upload->setTypesAllowed(['jpg', 'jpeg', 'png']);   // Only images
$photo = $upload->save('photo', 'uploads');         // Save file

if ($upload->hasError()) {
    echo $upload->getErrorMessage();
} else {
    echo 'File uploaded: ' . $photo;
}

Clase FileUpload

Aprovechando que hemos preparado una clase para upload, vamos a generalizarla, documentarla, versionarla y publicarla en GitHub como un proyecto abierto, lo pueden encontrar en: https://github.com/kodetop/PHP-FileUpload. Si tienen propuestas de mejora o encuentran defectos lo pueden reportar para mejorar la clase.

Buenas Prácticas

Almacenar Fuera del Directorio Web
Para mayor seguridad, almacena los archivos subidos fuera del directorio accesible públicamente. Esto previene la ejecución directa de scripts subidos maliciosamente.

Validación del Nombre del Archivo
Es recomendable limpiar y validar el nombre del archivo para evitar problemas de seguridad, como la inclusión de rutas maliciosas.

Uso de Identificadores Únicos
Para evitar colisiones de nombres de archivo, puedes agregar un identificador único al nombre del archivo subido.

Valida los archivos subidos
Nunca confíes en los archivos subidos por los usuarios, valida las extensiones, los tipos MIME y evita cualquier tipo de archivo ejecutable.

Conclusión

Implementar el upload de archivos en PHP puede parecer una tarea sencilla, pero requiere atención a detalles de seguridad para evitar vulnerabilidades. Siguiendo estas prácticas, puedes asegurar que tu aplicación maneje los uploads de manera segura y eficiente.

Referencias

Envíar Comentario

En este sitio los comentarios se publican previa aprobación del equipo de Kodetop. Evita los comentarios ofensivos, obscenos o publicitarios. Si deseas publicar código fuente puedes hacerlo entre las etiquedas <pre></pre>