Subir ficheros al servidor con PHP 8

Sube ficheros al servidor con PHP 8 de forma segura: move_uploaded_file() en lugar del inseguro copy() original, validación de MIME real con finfo, lista blanca de extensiones, límite de tamaño configurable y nombre aleatorio para evitar colisiones y path traversal. El ZIP incluye también un .htaccess para proteger el directorio de uploads contra la ejecución de scripts.
				<?php
/**
* Subida de ficheros al servidor — PHP 8
*
* Autor: David Carrero — https://programacion.net
* Original: programacion.net (2003)
*
* Versión actualizada con move_uploaded_file(), validación de MIME real,
* lista blanca de extensiones y nombre aleatorio para el fichero destino.
* El original usaba copy() sobre $HTTP_GET_VARS, inseguro y eliminado en PHP 7.
*/

// ——— Configuración ———————————————————————————————————————
define('UPLOAD_DIR', __DIR__ . '/uploads/');
define('MAX_BYTES', 5 * 1024 * 1024); // 5 MB

const EXTENSIONES = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'zip'];
const MIMES = [
'image/jpeg', 'image/png', 'image/gif',
'application/pdf', 'text/plain', 'application/zip',
];
// —————————————————————————————————————————————————————————

$mensaje = '';
$estado = ''; // 'ok' | 'error'

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fichero'])) {
$f = $_FILES['fichero'];

try {
// 1. Errores internos de PHP (tamaño en php.ini, subida incompleta…)
if ($f['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException(match ($f['error']) {
UPLOAD_ERR_INI_SIZE,
UPLOAD_ERR_FORM_SIZE => 'El fichero supera el tamaño máximo permitido.',
UPLOAD_ERR_PARTIAL => 'La subida se interrumpió antes de completarse.',
UPLOAD_ERR_NO_FILE => 'No se seleccionó ningún fichero.',
default => "Error en la subida (código {$f['error']}).",
});
}

// 2. Tamaño máximo propio (independiente de php.ini)
if ($f['size'] > MAX_BYTES) {
throw new RuntimeException('El fichero supera el límite de 5 MB.');
}

// 3. Extensión (lista blanca)
$ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
if (!in_array($ext, EXTENSIONES, true)) {
throw new RuntimeException("Extensión .{$ext} no permitida.");
}

// 4. Tipo MIME real — se lee del fichero en disco, no del navegador
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
if (!in_array($mime, MIMES, true)) {
throw new RuntimeException("Tipo de fichero no permitido ({$mime}).");
}

// 5. Crear directorio de destino si no existe
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}

// 6. Nombre aleatorio para evitar sobreescrituras y path traversal
$nombreFinal = bin2hex(random_bytes(8)) . '.' . $ext;
$destino = UPLOAD_DIR . $nombreFinal;

// 7. move_uploaded_file() verifica que el fichero proviene de una
// subida HTTP real (evita ataques de fichero arbitrario)
if (!move_uploaded_file($f['tmp_name'], $destino)) {
throw new RuntimeException('No se pudo mover el fichero al directorio destino.');
}

$mensaje = "Fichero guardado como <strong>{$nombreFinal}</strong>.";
$estado = 'ok';

} catch (RuntimeException $e) {
$mensaje = htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
$estado = 'error';
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Subir fichero al servidor</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto;
padding: 0 1rem; background: #f5f5f5; color: #222; }
form { background: #fff; padding: 1.5rem; border: 1px solid #ddd;
border-radius: 6px; margin-bottom: 1.5rem; }
label { font-weight: bold; }
input[type="file"] { display: block; margin: .6rem 0 .3rem; }
input[type="submit"] { margin-top: .75rem; padding: .4rem 1.4rem; cursor: pointer; }
.msg { padding: .75rem 1rem; border-radius: 4px; margin-bottom: 1rem; }
.ok { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
small { color: #666; font-size: .875rem; }
code { background: #eee; padding: .1em .3em; border-radius: 3px; }
ul li { margin-bottom: .4rem; }
</style>
</head>
<body>

<h1>Subir fichero al servidor</h1>

<?php if ($mensaje !== ''): ?>
<div class="msg <?= $estado ?>"><?= $mensaje ?></div>
<?php endif; ?>

<form method="post" enctype="multipart/form-data">
<label for="fichero">Selecciona un fichero:</label>
<input type="file" id="fichero" name="fichero">
<small>JPG, PNG, GIF, PDF, TXT, ZIP — máximo 5 MB</small>
<input type="submit" value="Subir fichero">
</form>

<h2>Cómo funciona</h2>
<ul>
<li><code>move_uploaded_file()</code> verifica que el fichero procede de una subida HTTP real, no de una ruta arbitraria del servidor.</li>
<li>El tipo MIME se comprueba con <code>finfo</code> sobre el contenido real del fichero, no sobre el nombre que envía el navegador (que se puede falsificar fácilmente).</li>
<li>El nombre del fichero se sustituye por un hash aleatorio con <code>random_bytes()</code> para evitar colisiones y ataques de path traversal.</li>
<li>Protege el directorio <code>uploads/</code> con un <code>.htaccess</code> que impida ejecutar scripts dentro de él (ver fichero adjunto en el ZIP).</li>
</ul>

</body>
</html>
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
TUTORIAL ANTERIOR