Upload sicuro dei file in PHP: validazione, archiviazione e protezione

by theArchitect
SHARE
Upload sicuro dei file in PHP: validazione, archiviazione e protezione
© Guida-HTML5.it

Introduzione

La gestione degli upload è una delle operazioni più comuni nelle applicazioni PHP: immagini profilo, documenti, allegati alle richieste di assistenza e file multimediali vengono spesso inviati dal browser al server. Tuttavia, accettare un file non significa soltanto salvarlo in una cartella. Un upload non validato può consentire l’invio di file eseguibili, file troppo grandi, contenuti con estensioni alterate o nomi pericolosi.

In questo tutorial realizzeremo un esempio pratico per caricare un documento in PHP in modo più sicuro. L’applicazione controllerà la presenza di errori, la dimensione massima, il tipo MIME reale e l’estensione consentita. Inoltre genererà un nome casuale, evitando di usare direttamente il nome fornito dall’utente.

L’esempio utilizzerà il meccanismo standard degli upload HTTP multipart, la variabile globale $_FILES e la funzione move_uploaded_file(). Quest’ultima verifica che il file sia stato realmente caricato tramite HTTP POST prima di spostarlo nella destinazione finale.

Codice completo

<?php
// upload.php

// Cartella di destinazione: deve esistere ed essere scrivibile dal server web.
$cartellaUpload = __DIR__ . ´/uploads´;

// Dimensione massima consentita: 5 MB.
$dimensioneMassima = 5 * 1024 * 1024;

// MIME consentiti associati alle relative estensioni finali.
$tipiConsentiti = [
    ´image/jpeg´ => ´jpg´,
    ´image/png´  => ´png´,
    ´application/pdf´ => ´pdf´
];

$messaggio = ´´;
$errore = ´´;

if ($_SERVER[´REQUEST_METHOD´] === ´POST´) {
    if (!isset($_FILES[´documento´])) {
        $errore = ´Nessun file ricevuto.´;
    } else {
        $file = $_FILES[´documento´];

        // Controlla gli errori generati durante il caricamento HTTP.
        if ($file[´error´] !== UPLOAD_ERR_OK) {
            $errore = ´Il caricamento non è stato completato correttamente.´;
        } elseif ($file[´size´] > $dimensioneMassima) {
            $errore = ´Il file supera la dimensione massima di 5 MB.´;
        } elseif (!is_uploaded_file($file[´tmp_name´])) {
            $errore = ´Il file ricevuto non proviene da un upload valido.´;
        } else {
            // Rileva il MIME reale analizzando il contenuto del file.
            $finfo = new finfo(FILEINFO_MIME_TYPE);
            $mimeReale = $finfo->file($file[´tmp_name´]);

            if (!array_key_exists($mimeReale, $tipiConsentiti)) {
                $errore = ´Il formato del file non è consentito.´;
            } else {
                // Genera un nome casuale, senza usare quello inviato dal client.
                $nomeCasuale = bin2hex(random_bytes(16));
                $estensione = $tipiConsentiti[$mimeReale];
                $nomeFinale = $nomeCasuale . ´.´ . $estensione;
                $percorsoFinale = $cartellaUpload . ´/´ . $nomeFinale;

                if (!is_dir($cartellaUpload)) {
                    mkdir($cartellaUpload, 0750, true);
                }

                if (move_uploaded_file($file[´tmp_name´], $percorsoFinale)) {
                    $messaggio = ´File caricato correttamente.´;
                } else {
                    $errore = ´Impossibile salvare il file sul server.´;
                }
            }
        }
    }
}
?>

<!doctype html>
<html lang="it">
<head>
    <meta charset="utf-8">
    <title>Upload sicuro</title>
</head>
<body>
    <h1>Carica un documento</h1>

    <?php if ($messaggio !== ´´): ?>
        <p><strong><?= htmlspecialchars($messaggio, ENT_QUOTES, ´UTF-8´) ?></strong></p>
    <?php endif; ?>

    <?php if ($errore !== ´´): ?>
        <p><strong><?= htmlspecialchars($errore, ENT_QUOTES, ´UTF-8´) ?></strong></p>
    <?php endif; ?>

    <form method="post" enctype="multipart/form-data">
        <label for="documento">PDF, JPG o PNG:</label>
        <input type="file" id="documento" name="documento" required>
        <button type="submit">Carica</button>
    </form>
</body>
</html>

Spiegazione

Il form HTML

Per inviare un file il form deve utilizzare l’attributo enctype="multipart/form-data". Senza questo attributo il browser invierebbe soltanto i campi testuali e il file non sarebbe disponibile in $_FILES. Il metodo POST è preferibile perché evita di inserire i dati dell’upload nell’URL.

La struttura di $_FILES

Per ogni campo file, PHP rende disponibili alcune informazioni importanti:

  • name: nome originale fornito dal client;
  • type: tipo dichiarato dal browser, quindi non affidabile da solo;
  • tmp_name: percorso del file temporaneo sul server;
  • error: eventuale codice di errore dell’upload;
  • size: dimensione del file in byte.

Il nome originale non viene utilizzato per creare il percorso finale. Un utente potrebbe inviare un nome contenente sequenze come ../, caratteri speciali o un’estensione ingannevole. La scelta più sicura consiste nel generare un identificatore casuale tramite random_bytes().

Controllo del tipo reale

Il valore $_FILES[´documento´][´type´] può essere manipolato dal client. Per questo l’esempio usa la classe finfo, che analizza il contenuto e restituisce un MIME più attendibile. Il codice utilizza una whitelist: sono accettati soltanto JPEG, PNG e PDF.

È importante associare internamente ogni MIME all’estensione desiderata. In questo modo l’estensione finale non viene copiata dall’utente, ma viene determinata dall’applicazione dopo la validazione.

Spostamento del file

Il file viene inizialmente salvato da PHP in una posizione temporanea. La funzione move_uploaded_file() lo trasferisce nella cartella definitiva. Prima dello spostamento viene verificato anche is_uploaded_file(), utile per assicurarsi che il percorso temporaneo rappresenti un upload HTTP valido.

Best practice

  • Usa una whitelist: definisci esplicitamente i formati accettati invece di bloccare soltanto alcune estensioni pericolose.
  • Limita la dimensione: applica un controllo nel codice e configura anche upload_max_filesize e post_max_size nel file php.ini.
  • Non fidarti del nome originale: genera nomi casuali e prevedibili solo internamente.
  • Preferisci una cartella non eseguibile: quando possibile, salva i file fuori dalla document root. In alternativa configura il web server affinché non interpreti script nella cartella di upload.
  • Proteggi l’accesso: se i documenti sono privati, non pubblicarli direttamente con un URL. Servili attraverso uno script che controlli autenticazione e autorizzazioni.
  • Valida anche il contenuto: per immagini realmente valide puoi usare funzioni come getimagesize() o le librerie grafiche appropriate.
  • Gestisci gli errori senza dettagli sensibili: mostra messaggi comprensibili all’utente, ma registra informazioni tecniche in un sistema di monitoraggio adeguato.
  • Considera la scansione antivirus: in applicazioni che ricevono documenti da utenti sconosciuti, integra un controllo antimalware prima di rendere disponibile il file.

La validazione lato client, ad esempio tramite l’attributo HTML accept, può migliorare l’esperienza utente ma non sostituisce mai i controlli lato server. Un client può infatti ignorare o modificare qualsiasi indicazione presente nel browser.

Riepilogo

Un upload affidabile richiede più passaggi: ricevere il file con un form configurato correttamente, controllare il codice di errore, limitare la dimensione, verificare il MIME reale, generare un nome sicuro e spostare il file con move_uploaded_file(). La strategia più importante è non fidarsi dei dati forniti dal client, inclusi nome, estensione e tipo dichiarato.

Questo approccio costituisce una base solida per immagini, PDF e altri documenti. In un progetto reale bisogna aggiungere autenticazione, autorizzazioni, protezione CSRF, eventuale scansione antivirus e una politica chiara per la conservazione e la cancellazione dei file.

Approfondisci con risorse ufficiali

SHARE