📁 File Uploads seguros — validacion y almacenamiento
Actualizado: 2024-12-01
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();
}
Descargar en Markdown
Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.