Un modulo di caricamento che accetta immagini è un modo per posare un file sul suo server. La validazione riduce ciò che passa; non rende sicura la directory. A renderla sicura è che il server web si rifiuti di ESEGUIRE qualsiasi cosa lì dentro, qualunque cosa il file si riveli essere.

Nginx

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

Apache e LiteSpeed

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

Validi anche, e come si deve

  • Controlli il tipo vero, non il nome mandato da un browser: finfo_file() in PHP.
  • Rinomini ogni caricamento con un nome generato da lei. Non conservi mai il nome di file del visitatore.
  • Rifiuti le doppie estensioni: un file chiamato invoice.php.jpg non è una svista.
  • Ricodifichi le immagini. Far passare una foto attraverso una libreria grafica toglie tutto ciò che vi era nascosto 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';

Meglio ancora: le conservi dove il web non arriva

Tenga i caricamenti fuori dal document root e li serva tramite uno script che controlla chi sta chiedendo. Un millisecondo più lento, e allora non esiste più alcun URL capace di eseguire qualcosa.

Lo provi. Metta lei stesso un file PHP innocuo nella directory dei caricamenti e lo richieda: deve ottenere un 403 o il testo sorgente, mai l'output. Ci vuole un minuto ed è l'unica prova.