In breve: quando un browser carica un file, invia una richiesta POST in formato multipart/form-data, in cui i campi del modulo e il contenuto dei file si susseguono come parti distinte. PHP scompone da sé questa richiesta, scrive ogni parte-file in un file temporaneo in upload_tmp_dir, registra i dati in $_FILES e solo allora avvia il Suo script. Lo script sposta il file con move_uploaded_file(); tutto ciò che non viene spostato, PHP lo elimina al termine della richiesta.
Il percorso di un file dal browser allo script
Per chi decide, una cosa conta più di tutte: un upload non è un'operazione della sola applicazione. Gran parte del lavoro avviene nell'interprete PHP, prima ancora che il codice del CMS o dello shop entri in gioco. Per questo gli errori in questa parte di PHP riguardano ogni sito web, che abbia o no un modulo di upload.
Lo svolgimento in sintesi:
- Il modulo con
method="POST"eenctype="multipart/form-data"viene inviato. - Il server web accetta la richiesta e la passa a PHP, a seconda della modalità di esecuzione tramite PHP-FPM, mod_php o un'altra interfaccia.
- PHP controlla limiti come
post_max_sizee scompone il contenuto in base a una sequenza di separazione, il cosiddetto boundary. - I campi normali finiscono in
$_POST, le parti-file vengono scritte in file temporanei e descritte in$_FILES. - Solo adesso parte lo script, che controlla il file e lo sposta nella destinazione.
- Al termine della richiesta PHP elimina tutti i file temporanei che non sono stati spostati.
multipart/form-data: la struttura di una richiesta di upload
Una richiesta di upload è puro testo con dati di file incorporati. L'header Content-Type indica il boundary, e nel contenuto questa sequenza di caratteri separa le singole parti. Ogni parte ha intestazioni proprie con il nome del campo e, se presente, il nome del file. Un esempio molto abbreviato e innocuo:
POST /bewerbung HTTP/1.1
Content-Type: multipart/form-data; boundary=----trenner42
------trenner42
Content-Disposition: form-data; name="vorname"
Erika
------trenner42
Content-Disposition: form-data; name="lebenslauf"; filename="lebenslauf.pdf"
Content-Type: application/pdf
%PDF-1.7 ... Dateiinhalt ...
------trenner42--
Tutti i dati di questo testo provengono dal mittente, compresi il nome del file e il Content-Type della parte. PHP li riporta in $_FILES, ma non ne verifica la plausibilità.
L'array $_FILES
Per un campo di nome lebenslauf, secondo il manuale PHP $_FILES['lebenslauf'] contiene queste chiavi:
| Chiave | Contenuto | Affidabile? |
|---|---|---|
name |
nome originale del file presso il mittente | no |
full_path |
percorso comunicato dal browser, da PHP 8.1.0 | no |
type |
tipo MIME secondo il browser, non verificato da PHP | no |
size |
dimensione in byte | determinata da PHP |
tmp_name |
percorso del file temporaneo sul server | assegnato da PHP |
error |
codice di errore dell'elaborazione dell'upload | impostato da PHP |
Nei campi con parentesi quadre nel nome, ad esempio bilder[], ogni chiave contiene un array con una voce per file. L'applicazione scorre allora, ad esempio, $_FILES['bilder']['error'] e accede con lo stesso indice a tmp_name e name.
Un modulo può inoltre inviare, prima del campo file, un campo nascosto MAX_FILE_SIZE. Secondo il manuale è solo un suggerimento per il browser, facile da aggirare. Può fare affidamento soltanto sui limiti che il server fa rispettare: in PHP, nel server web e nell'applicazione stessa.
Limiti: le direttive di php.ini per gli upload
Queste direttive governano l'elaborazione degli upload. I valori predefiniti provengono dal manuale PHP e valgono quando nessun php.ini li sovrascrive.
| Direttiva | Predefinito | Modificabile in |
|---|---|---|
file_uploads |
1 |
solo configurazione di sistema |
upload_tmp_dir |
vuoto (temp di sistema) | solo configurazione di sistema |
upload_max_filesize |
2M |
php.ini, pool, configurazione per directory |
post_max_size |
8M |
php.ini, pool, configurazione per directory |
max_file_uploads |
20 |
php.ini, pool, configurazione per directory |
enable_post_data_reading |
1 |
php.ini, pool, configurazione per directory |
post_max_size deve essere maggiore di upload_max_filesize, e secondo il manuale memory_limit dovrebbe di norma essere maggiore di post_max_size. Se upload_tmp_dir non è scrivibile, PHP ripiega sulla directory temp del sistema. Con open_basedir la directory temp deve essere consentita, altrimenti gli upload falliscono. I valori pratici e le loro conseguenze sono descritti nell'articolo Limitare in modo sensato post_max_size e upload_max_filesize.
Valutare correttamente i codici di errore
Il campo error indica che cosa è successo durante l'elaborazione. Le costanti vanno da UPLOAD_ERR_OK (0), passando per UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) e UPLOAD_ERR_NO_FILE (4), fino a UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) e UPLOAD_ERR_EXTENSION (8).
<?php
declare(strict_types=1);
$datei = $_FILES['lebenslauf'] ?? null;
if ($datei === null) {
exit('Keine Upload-Daten, eventuell wurde post_max_size überschritten.');
}
$meldung = match ($datei['error']) {
UPLOAD_ERR_OK => null,
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Datei zu groß.',
UPLOAD_ERR_NO_FILE => 'Keine Datei gewählt.',
default => 'Upload fehlgeschlagen.',
};
if ($meldung === null && is_uploaded_file($datei['tmp_name'])) {
$ziel = '/srv/app/uploads/' . bin2hex(random_bytes(16)) . '.pdf';
move_uploaded_file($datei['tmp_name'], $ziel);
}
L'esempio mostra solo il meccanismo. Il controllo del tipo di file, la collocazione fuori dal webroot e il blocco dell'esecuzione degli script sono descritti nella guida Implementare in modo sicuro gli upload di file in PHP.
Upload e PHP2Shell: che cosa significano le misure transitorie
Le vulnerabilità PHP2Shell CVE-2026-109618 e CVE-2026-109619 riguardano l'elaborazione che avviene attorno alla richiesta nell'interprete PHP. Per questo php.net indica come misure transitorie, tra l'altro, post_max_size = 8M (senza upload) oppure 32M (con upload), upload_max_filesize al di sotto di questo valore, oltre a session.upload_progress.enabled=0. Questi valori vanno inseriti in php.ini, nel pool FPM o nella configurazione del server; poi tutti i worker devono essere riavviati.
Alcuni autori di settore propongono inoltre di bloccare nel proxy o nel WAF le richieste multipart/form-data al di fuori dei veri percorsi di upload. È una misura non ufficiale, con il rischio di rompere dei moduli; la valutazione è descritta nell'articolo Filtrare le richieste di upload al perimetro. Nessuna di queste misure sostituisce l'aggiornamento a PHP 8.5.12, 8.4.27, 8.3.36 o 8.2.35.
Come aiuta concretamente PHP SOS?
- Verifichiamo quali dei Suoi siti web hanno davvero bisogno di upload e su quali percorsi.
- Impostiamo i limiti e le misure transitorie per ogni pool, riavviamo tutti i worker e testiamo i moduli.
- Controlliamo che nelle directory di upload non ci siano file eseguibili e blocchiamo l'esecuzione degli script nell'ambito del rafforzamento del server.
- Installiamo l'aggiornamento di PHP e documentiamo lo stato delle patch per ogni sito web.
Domande frequenti
Dove salva PHP per prima cosa i file caricati?
In una directory temporanea. Per impostazione predefinita è la directory temporanea del sistema; con upload_tmp_dir si può cambiare. Se lo script non sposta il file, PHP lo elimina al termine della richiesta.
Posso fidarmi del campo type in $_FILES?
No. Il tipo MIME lo comunica il browser, e secondo il manuale PHP non lo verifica. Anche name e full_path provengono dal mittente. Il tipo effettivo del file lo determina sul server in base al contenuto.
Perché $_FILES è vuoto anche se è stato inviato un file?
Di solito l'intera richiesta era più grande di post_max_size. In tal caso PHP svuota completamente $_POST e $_FILES, senza alcun codice di errore nell'array. Altre cause sono un enctype mancante nel modulo o file_uploads disattivato.
Vietare gli upload nell'applicazione aiuta contro PHP2Shell?
No. L'elaborazione dei dati della richiesta avviene in PHP, prima che giri il codice dell'applicazione. Sono efficaci l'aggiornamento di PHP e il riavvio di tutti i worker; limiti più stretti sono soltanto una misura transitoria.
Qual è la differenza tra upload_max_filesize e post_max_size?
upload_max_filesize limita ogni singolo file, post_max_size l'intero contenuto della richiesta, inclusi tutti i file e i campi. Per questo post_max_size deve essere maggiore di upload_max_filesize.