Situación actual: PHP2Shell. Dos vulnerabilidades críticas de PHP (CVSS 9.2) permiten ejecutar código malicioso sin autenticación. Parches desde el 21/10/2026.Qué hacer ahora →

Subida segura de archivos en PHP: seguridad de uploads

Los formularios de subida son una vía frecuente para las webshells. Cómo comprobar bien los tipos de archivo, guardar los archivos con seguridad e impedir que el servidor ejecute scripts subidos.

Actualizado 7 min de lecturaGuías

En resumen: en la seguridad de uploads en PHP no decide una única comprobación, sino la combinación: verificar el tipo de archivo en el servidor, renombrar los archivos, guardarlos fuera del webroot y no permitir la ejecución de scripts en la carpeta de subidas. Un formulario de subida sin verificar es una de las vías clásicas por las que los atacantes introducen una puerta trasera en un servidor web.

Por qué los formularios de subida son una puerta de entrada preferida

Todo formulario que acepta archivos escribe datos ajenos en su servidor. Entre ellos están los formularios de candidatura, los portales de clientes, las galerías de imágenes, las áreas de soporte y muchas extensiones de CMS y tiendas. Si un atacante consigue dejar allí un archivo que el servidor web ejecuta después como PHP, dispone de una puerta trasera persistente.

Para la dirección, esto significa que una subida de currículos de apariencia inofensiva puede, en el peor de los casos, llevar a la toma de control de todo el servidor web. Además, en las carpetas de subidas suele haber datos personales que, con una configuración incorrecta, pueden consultarse públicamente.

Para administradores y desarrolladores significa que el código de la aplicación y la configuración del servidor deben encajar. Una debilidad en un nivel suele quedar compensada por el otro si ambos están bien aplicados. Cómo encontrar archivos ya introducidos a escondidas lo describe la guía Detectar y eliminar webshells.

Las reglas básicas de la seguridad de uploads en PHP

Las reglas siguientes corresponden a las recomendaciones de las OWASP Cheat Sheets y han dado buen resultado en la práctica:

  • Lista de permitidos en lugar de lista de bloqueo: aceptar solo tipos de archivo expresamente permitidos, por ejemplo PDF, JPG y PNG. Las listas de bloqueo de extensiones sueltas se eluden con variantes y dobles extensiones.
  • Comprobar el contenido, no solo la extensión: determinar el tipo real en el servidor con finfo. El tipo MIME de $_FILES es un dato del remitente y no es fiable.
  • Nombre de archivo nuevo: no usar nunca el nombre original como nombre de archivo. Generar un nombre aleatorio y añadir la extensión de la lista de permitidos.
  • Almacenar fuera del webroot: guardar los archivos de modo que no sean accesibles directamente por URL. La entrega se hace mediante un script con comprobación de autorización.
  • Limitar el tamaño: en la aplicación, en PHP y en el servidor web.
  • Sin ejecución: si los uploads deben estar en el webroot, desactivar la ejecución de scripts en ese directorio.
  • Comprobar solo en el servidor: las comprobaciones con JavaScript en el navegador mejoran el manejo, pero no protegen.

Un ejemplo seguro con finfo y nombre aleatorio

El ejemplo siguiente muestra el núcleo de un procesamiento seguro. Es deliberadamente escueto y debe integrarse en su aplicación, es decir, con inicio de sesión, comprobación de autorización, protección CSRF y registro.

<?php
declare(strict_types=1);

const MAX_BYTES  = 5 * 1024 * 1024; // 5 MB
const ERLAUBT    = [
    'application/pdf' => 'pdf',
    'image/jpeg'      => 'jpg',
    'image/png'       => 'png',
];
const ZIELORDNER = '/var/www/daten/uploads'; // fuera del webroot

$datei = $_FILES['dokument'] ?? null;

if (!is_array($datei) || $datei['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit('Upload fehlgeschlagen.');
}

if ($datei['size'] > MAX_BYTES || !is_uploaded_file($datei['tmp_name'])) {
    http_response_code(400);
    exit('Datei ungültig.');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$typ   = $finfo->file($datei['tmp_name']);

if (!isset(ERLAUBT[$typ])) {
    http_response_code(415);
    exit('Dateityp nicht erlaubt.');
}

$name = bin2hex(random_bytes(16)) . '.' . ERLAUBT[$typ];
$ziel = ZIELORDNER . '/' . $name;

if (!move_uploaded_file($datei['tmp_name'], $ziel)) {
    http_response_code(500);
    exit('Speichern fehlgeschlagen.');
}

chmod($ziel, 0640);
// Guardar el nombre original solo como texto en la base de datos
// y escaparlo con htmlspecialchars() en cada salida.

En el caso de las imágenes merece la pena un paso más: volver a codificar la imagen con GD o Imagick descartando los metadatos. Eso no sustituye a las demás comprobaciones, pero reduce el riesgo residual por datos ajenos incrustados.

Para documentos de fuentes desconocidas, como candidaturas o fotos de reclamaciones, es razonable además un análisis antivirus antes de reenviarlos. Protege menos al servidor web que a los empleados que luego abren los archivos en sus equipos de trabajo. Registre además cada subida con hora, cuenta, tamaño de archivo y tipo detectado, para poder reconstruir más adelante patrones inusuales.

En los CMS y las tiendas, las bibliotecas de medios integradas suelen incorporar sus propias comprobaciones. Son más arriesgadas las extensiones de formularios, las funciones de importación y las páginas adicionales programadas a medida que guardan archivos sin pasar por la biblioteca. Precisamente esos puntos deberían revisarse primero en una auditoría.

Consejo

Entregue los archivos de la carpeta protegida con Content-Disposition: attachment y X-Content-Type-Options: nosniff. Entonces el navegador no muestra directamente los contenidos problemáticos, sino que los ofrece como descarga.

Desactivar la ejecución de PHP en la carpeta de subidas

Aunque se cuele un archivo con contenido erróneo, el servidor web no debe ejecutarlo como script. Esta segunda capa de protección debe estar en todo servidor que guarde uploads en el webroot.

Para Nginx con PHP-FPM:

location ^~ /uploads/ {
    location ~* \.(php|phtml|phar)$ {
        deny all;
    }
}

Para Apache, mejor directamente en el VirtualHost que en un .htaccess, para que la aplicación no pueda sobrescribir la regla:

<Directory "/var/www/html/uploads">
    AllowOverride None
    <FilesMatch "\.(php|phtml|phar)$">
        Require all denied
    </FilesMatch>
</Directory>

Si PHP se ejecuta como módulo de Apache (mod_php), puede establecer además php_admin_flag engine off en el mismo bloque. Tras el cambio, compruebe con un archivo de prueba inofensivo que solo muestre un texto que el servidor responde con 403 a una petición sobre él.

Límites de tamaño y la relación con PHP2Shell

Las subidas están estrechamente ligadas a los ajustes de PHP post_max_size y upload_max_filesize. En el contexto de PHP2Shell, php.net recomienda como medida transitoria fijar post_max_size en 8M si no se necesitan subidas, o en 32M con subidas, y mantener upload_max_filesize por debajo. No es una protección completa y no sustituye a la actualización a una versión corregida de PHP.

; php.ini o pool de FPM (php_admin_value), después reiniciar todos los workers
file_uploads        = On
post_max_size       = 32M
upload_max_filesize = 20M
max_file_uploads    = 5

Si un sitio web no necesita subidas en absoluto, file_uploads = Off es la solución más limpia. Qué ajustes son razonables hasta el parche se explica en Medidas inmediatas sin parche. Los permisos adecuados para los directorios de subidas los describe Permisos de archivos en el servidor web.

Errores típicos en las funciones de subida

Al revisar aplicaciones existentes aparecen una y otra vez los mismos patrones. Revise sus formularios en busca de estos puntos:

  • La extensión solo se comprueba en parte, de modo que se cuelan nombres de archivo con varias extensiones.
  • El nombre original del formulario acaba en la ruta de almacenamiento. Eso abre vías para la manipulación de rutas y la sobrescritura de archivos existentes.
  • Los uploads están públicos con nombres previsibles. En candidaturas o contratos, eso es además un problema de protección de datos.
  • La carpeta de subidas permite escritura al servidor web y es a la vez ejecutable.
  • Las extensiones de subida antiguas o los plugins de formularios no se actualizan, aunque el fabricante haya publicado actualizaciones de seguridad.
  • No se registran los errores. Sin embargo, las subidas rechazadas son una señal temprana de intentos dirigidos.

¿Cómo ayuda PHP SOS en concreto?

  • Revisamos sus funciones de subida en el código y en la configuración del servidor y señalamos las debilidades concretas con su prioridad.
  • Aplicamos el bloqueo de ejecución en la carpeta de subidas, los permisos adecuados y los límites de tamaño en el marco del refuerzo del servidor.
  • Si hay sospechas, buscamos scripts introducidos en los directorios de subidas y los limpiamos mediante nuestra eliminación de malware.
  • Si lo desea, hacemos contrastar la aplicación de forma independiente mediante una prueba de penetración.

Preguntas frecuentes

¿Basta con comprobar la extensión del archivo al subirlo?

No. La extensión y el tipo MIME que comunica el navegador proceden del remitente y se pueden elegir libremente. Compruebe el contenido en el servidor, asigne un nombre de archivo nuevo y bloquee la ejecución de scripts en el directorio de subidas.

¿Dónde deben guardarse los archivos subidos?

Lo mejor es fuera del directorio web público. La entrega la asume entonces un script que comprueba antes si el usuario puede ver el archivo. Así, los archivos no se pueden abrir ni ejecutar directamente.

¿Son inofensivos los archivos SVG como imágenes?

No automáticamente. SVG es XML y puede contener contenido activo. Admita SVG solo si depura los archivos o los entrega exclusivamente como descarga con la cabecera Content-Disposition adecuada.

¿Tiene el tamaño de subida algo que ver con PHP2Shell?

Sí, de forma indirecta. Como medida transitoria, php.net recomienda valores más bajos para post_max_size y upload_max_filesize. Eso reduce el riesgo, pero no sustituye a la actualización a una versión corregida de PHP ni al reinicio de todos los workers.

Fuentes

Contacto inmediato

Cada hora cuenta. Escríbanos ahora.

Describa brevemente el sistema, la versión de PHP y lo que ha observado. Nos pondremos en contacto, aclararemos la situación y trabajaremos en remoto hasta que el incidente quede cerrado y documentado.

Ayuda inmediata