Un formulario de subida que acepta imágenes es una manera de poner un archivo en su servidor. La validación reduce lo que pasa; no vuelve seguro el directorio. Lo que lo vuelve seguro es que el servidor web se niegue a EJECUTAR nada allí, sea cual sea el archivo.

Nginx

location ^~ /uploads/ {
    location ~ \.php$ { deny all; }
    add_header X-Content-Type-Options nosniff;
}

Apache y LiteSpeed

# /var/www/site/uploads/.htaccess
php_flag engine off
<FilesMatch "\.(php|phar|phtml|cgi|pl)$">
    Require all denied
</FilesMatch>
Options -ExecCGI -Indexes

Valide además, y bien

  • Compruebe el tipo real, no el nombre que mandó un navegador: finfo_file() en PHP.
  • Renombre cada subida con algo generado por usted. No conserve nunca el nombre de archivo del visitante.
  • Rechace las extensiones dobles: un archivo llamado invoice.php.jpg no es una casualidad.
  • Vuelva a codificar las imágenes. Pasar una foto por una biblioteca de imágenes le quita todo lo que hubiera escondido dentro.
$f = new finfo(FILEINFO_MIME_TYPE);
$type = $f->file($tmp);
if (!in_array($type, ['image/jpeg','image/png','image/webp'], true)) {
    // reject
}
$name = bin2hex(random_bytes(16)) . '.webp';

Mejor todavía: guárdelas donde la web no llega

Mantenga las subidas fuera de la raíz de documentos y sírvalas mediante un script que compruebe quién pregunta. Un milisegundo más lento, y ya no existe ninguna URL capaz de ejecutar nada.

Pruébelo. Deje usted mismo un archivo PHP inofensivo en el directorio de subidas y pídalo: tiene que obtener un 403 o el texto fuente, nunca la salida. Esto cuesta un minuto y es la única prueba.