---
title: File Uploads seguros — validacion y almacenamiento
section: security
slug: file-uploads
description: "Como implementar uploads de archivos seguros en modulos PrestaShop: validacion MIME, tamano maximo, nombres aleatorios y directorio seguro."
keywords: prestashop upload archivo seguro MIME tamano validacion nombre aleatorio directorio modulo
last_updated: 2024-12-01
source_url: "https://ayudaprestashop.es/security/file-uploads"
---

# File Uploads seguros — validacion y almacenamiento

> Como implementar uploads de archivos seguros en modulos PrestaShop: validacion MIME, tamano maximo, nombres aleatorios y directorio seguro.

Los uploads de archivos son uno de los vectores de ataque mas peligrosos. Un atacante podria subir un archivo PHP y ejecutarlo en el servidor. La validacion estricta y el almacenamiento seguro son imprescindibles.

## Reglas de seguridad para uploads

| Regla | Razon |
| --- | --- |
| Nunca usar el nombre original del archivo | Puede contener caracteres peligrosos o exploits |
| Verificar MIME type con finfo, no con extension | La extension puede ser falsa (.php.jpg) |
| Establecer tamano maximo explicito | Previene ataques de denegacion de servicio |
| Almacenar fuera del webroot si es posible | Previene ejecucion directa del archivo |
| Agregar index.php al directorio de uploads | Previene listado de directorio |
| Nunca ejecutar archivos subidos | Usar readfile() para servirlos |
| Regenerar nombre con random_bytes() | Nombre impredecible evita acceso directo |

## Validacion de archivos

*Funcion de validacion completa de uploads*

```php
<?php

/**
 * Valida un archivo subido y lo mueve al directorio de uploads.
 *
 * @param array  $file         El elemento de $_FILES['campo']
 * @param array  $allowedMimes Lista de MIME types permitidos
 * @param int    $maxBytes     Tamano maximo en bytes
 * @param string $uploadDir    Directorio de destino (ruta absoluta)
 * @return string|false  Nombre del archivo guardado, o false si falla
 */
function secureUpload(array $file, array $allowedMimes, int $maxBytes, string $uploadDir)
{
    // 1. Verificar errores de PHP en el upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        $errors = [
            UPLOAD_ERR_INI_SIZE   => 'Archivo demasiado grande (php.ini)',
            UPLOAD_ERR_FORM_SIZE  => 'Archivo demasiado grande (formulario)',
            UPLOAD_ERR_PARTIAL    => 'Upload incompleto',
            UPLOAD_ERR_NO_FILE    => 'No se selecciono ningun archivo',
            UPLOAD_ERR_NO_TMP_DIR => 'Falta directorio temporal',
            UPLOAD_ERR_CANT_WRITE => 'Error de escritura en disco',
        ];
        throw new \RuntimeException($errors[$file['error']] ?? 'Error desconocido');
    }

    // 2. Verificar que es un upload real (no una ruta inyectada)
    if (!is_uploaded_file($file['tmp_name'])) {
        throw new \RuntimeException('Archivo no es un upload valido');
    }

    // 3. Verificar tamano
    if ($file['size'] > $maxBytes) {
        throw new \RuntimeException('Archivo demasiado grande: ' . round($file['size']/1024/1024, 2) . ' MB');
    }

    // 4. Verificar MIME type real (NO la extension, NO $file['type'])
    $finfo    = new \finfo(FILEINFO_MIME_TYPE);
    $realMime = $finfo->file($file['tmp_name']);
    if (!in_array($realMime, $allowedMimes, true)) {
        throw new \RuntimeException('Tipo de archivo no permitido: ' . $realMime);
    }

    // 5. Generar nombre de archivo seguro y aleatorio
    $ext      = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    $ext      = preg_replace('/[^a-z0-9]/', '', $ext); // Solo alfanumerico
    $filename = bin2hex(random_bytes(16)) . '.' . $ext; // Nombre impredecible

    // 6. Crear directorio si no existe y protegerlo
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
        // Prevenir listado de directorio y ejecucion de PHP
        file_put_contents($uploadDir . 'index.php', '<?php header("Location: ../");');
        file_put_contents($uploadDir . '.htaccess', "Options -Indexes\nphp_flag engine off");
    }

    // 7. Mover el archivo
    $destination = $uploadDir . $filename;
    if (!move_uploaded_file($file['tmp_name'], $destination)) {
        throw new \RuntimeException('Error al guardar el archivo');
    }

    return $filename;
}
```

## Upload de imagenes seguro

*Upload de imagenes con re-codificacion*

```php
<?php

/**
 * Upload de imagen con re-codificacion via GD para eliminar metadatos
 * y posibles payloads embebidos en la imagen.
 */
private function uploadImage(array $file): string
{
    $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    $maxSize      = 5 * 1024 * 1024; // 5MB
    $uploadDir    = _PS_MODULE_DIR_ . $this->name . '/uploads/img/';

    // Validar
    $filename = secureUpload($file, $allowedMimes, $maxSize, $uploadDir);
    $filepath = $uploadDir . $filename;

    // ── Re-codificar la imagen con GD para eliminar payloads ──
    $finfo    = new \finfo(FILEINFO_MIME_TYPE);
    $realMime = $finfo->file($filepath);

    $maxW = 1920;
    $maxH = 1920;

    switch ($realMime) {
        case 'image/jpeg':
            $source = imagecreatefromjpeg($filepath);
            break;
        case 'image/png':
            $source = imagecreatefrompng($filepath);
            break;
        case 'image/gif':
            $source = imagecreatefromgif($filepath);
            break;
        default:
            unlink($filepath);
            throw new \RuntimeException('Tipo de imagen no soportado');
    }

    if (!$source) {
        unlink($filepath);
        throw new \RuntimeException('Imagen corrupta o invalida');
    }

    // Redimensionar si es demasiado grande
    $w = imagesx($source);
    $h = imagesy($source);
    if ($w > $maxW || $h > $maxH) {
        $ratio  = min($maxW / $w, $maxH / $h);
        $newW   = (int) ($w * $ratio);
        $newH   = (int) ($h * $ratio);
        $resized = imagecreatetruecolor($newW, $newH);
        imagecopyresampled($resized, $source, 0, 0, 0, 0, $newW, $newH, $w, $h);
        imagedestroy($source);
        $source = $resized;
    }

    // Re-guardar (elimina metadatos EXIF y posibles payloads)
    imagejpeg($source, $filepath, 85);
    imagedestroy($source);

    return $filename;
}
```

## Upload de documentos PDF/CSV

*Upload de documentos no-imagen*

```php
<?php

// ── Para PDF y CSV — almacenar fuera del webroot ──
$uploadDir = dirname(_PS_ROOT_DIR_) . '/uploads/' . $this->name . '/docs/';
// o: /var/data/prestashop-uploads/{module}/ (fuera del document root)

$allowedMimes = [
    'application/pdf',
    'text/csv',
    'text/plain',
    'application/vnd.ms-excel',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
];

try {
    $filename = secureUpload(
        $_FILES['document'],
        $allowedMimes,
        10 * 1024 * 1024, // 10MB
        $uploadDir
    );

    // Guardar referencia en BD
    Configuration::updateValue('MYMODULE_UPLOADED_DOC', $filename);

} catch (\RuntimeException $e) {
    $this->errors[] = $e->getMessage();
}
```

## Servir archivos de forma segura

*Servir archivos sin exponer la ruta real*

```php
<?php

// En un FrontController o AdminController:
// Nunca redirigir directamente a la URL del archivo
// En cambio, servir a traves de PHP para verificar permisos

public function initContent(): void
{
    $filename = Tools::getValue('file');

    // Validar: solo alfanumerico y extension conocida
    if (!preg_match('/^[a-f0-9]{32}\.(pdf|csv|jpg|png)$/', $filename)) {
        header('HTTP/1.1 400 Bad Request');
        exit();
    }

    // Verificar que el usuario tiene acceso a este archivo
    if (!$this->userHasAccessToFile($filename)) {
        header('HTTP/1.1 403 Forbidden');
        exit();
    }

    $filepath = $uploadDir . $filename;
    if (!file_exists($filepath)) {
        header('HTTP/1.1 404 Not Found');
        exit();
    }

    // Servir el archivo
    $finfo    = new \finfo(FILEINFO_MIME_TYPE);
    $mime     = $finfo->file($filepath);
    header('Content-Type: ' . $mime);
    header('Content-Length: ' . filesize($filepath));
    header('Content-Disposition: attachment; filename="document.pdf"');
    header('Cache-Control: no-cache, no-store, must-revalidate');
    readfile($filepath);
    exit();
}
```


---

*Fuente: [https://ayudaprestashop.es/security/file-uploads](https://ayudaprestashop.es/security/file-uploads). Version Markdown generada automaticamente para consumo por LLMs.*
