Pe scurt: Când un browser încarcă un fișier, el trimite o cerere POST în formatul multipart/form-data, în care câmpurile formularului și conținutul fișierelor apar ca părți individuale, una după alta. PHP descompune singur această cerere, scrie fiecare parte de tip fișier într-un fișier temporar din upload_tmp_dir, introduce datele în $_FILES și abia apoi pornește scriptul dumneavoastră. Scriptul mută fișierul cu move_uploaded_file(); tot ce nu este mutat, PHP șterge la sfârșitul cererii.
Drumul unui fișier de la browser până în script
Pentru factorii de decizie, un lucru este esențial: un upload nu este o operațiune doar a aplicației. O mare parte din muncă se întâmplă în interpretorul PHP, înainte ca CMS-ul sau magazinul să fie implicat. De aceea, erorile din această parte a PHP afectează orice site, indiferent dacă are sau nu un formular de upload.
Desfășurarea, pe scurt:
- Formularul cu
method="POST"șienctype="multipart/form-data"este trimis. - Serverul web preia cererea și o transmite către PHP, în funcție de modul de operare prin PHP-FPM, mod_php sau altă interfață.
- PHP verifică limite precum
post_max_sizeși descompune conținutul pe baza unui șir de separare, numit boundary. - Câmpurile obișnuite ajung în
$_POST, părțile de tip fișier sunt scrise în fișiere temporare și descrise în$_FILES. - Abia acum pornește scriptul, verifică fișierul și îl mută la destinație.
- La sfârșitul cererii, PHP șterge toate fișierele temporare care nu au fost mutate.
multipart/form-data: structura unei cereri de upload
O cerere de upload este text simplu cu date de fișier încorporate. Antetul Content-Type indică boundary-ul, iar în conținut acest șir separă părțile individuale. Fiecare parte are propriile linii de antet, cu numele câmpului și, după caz, numele fișierului. Un exemplu foarte scurtat și inofensiv:
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--
Toate datele din acest text provin de la expeditor, inclusiv numele fișierului și Content-Type al părții. PHP le preia în $_FILES, dar nu le verifică din punct de vedere al plauzibilității.
Array-ul $_FILES
Pentru un câmp cu numele lebenslauf, $_FILES['lebenslauf'] conține, potrivit manualului PHP, aceste chei:
| Cheie | Conținut | De încredere? |
|---|---|---|
name |
numele original al fișierului la expeditor | nu |
full_path |
calea raportată de browser, din PHP 8.1.0 | nu |
type |
tip MIME conform browserului, neverificat de PHP | nu |
size |
dimensiunea în octeți | stabilită de PHP |
tmp_name |
calea fișierului temporar pe server | atribuită de PHP |
error |
codul de eroare al procesării upload-ului | setat de PHP |
La câmpurile cu paranteze pătrate în nume, de exemplu bilder[], fiecare cheie conține un array cu câte o intrare pentru fiecare fișier. Aplicația parcurge atunci, de exemplu, $_FILES['bilder']['error'] și accesează tmp_name și name prin același index.
Un formular poate trimite în plus, înaintea câmpului de fișier, un câmp ascuns MAX_FILE_SIZE. Potrivit manualului, acesta este doar o indicație pentru browser, care poate fi ocolită cu ușurință. Vă puteți baza exclusiv pe limitele impuse de server: în PHP, în serverul web și în aplicația însăși.
Limite: directivele php.ini pentru upload-uri
Aceste directive controlează procesarea upload-urilor. Valorile implicite provin din manualul PHP și se aplică atunci când niciun php.ini nu le suprascrie.
| Directivă | Implicit | Modificabilă în |
|---|---|---|
file_uploads |
1 |
doar configurația sistemului |
upload_tmp_dir |
gol (directorul temporar al sistemului) | doar configurația sistemului |
upload_max_filesize |
2M |
php.ini, pool, configurația directorului |
post_max_size |
8M |
php.ini, pool, configurația directorului |
max_file_uploads |
20 |
php.ini, pool, configurația directorului |
enable_post_data_reading |
1 |
php.ini, pool, configurația directorului |
post_max_size trebuie să fie mai mare decât upload_max_filesize, iar memory_limit ar trebui, potrivit manualului, să fie de regulă mai mare decât post_max_size. Dacă upload_tmp_dir nu poate fi scris, PHP revine la directorul temporar al sistemului. Cu open_basedir, directorul temporar trebuie permis, altfel upload-urile eșuează. Valorile practice și consecințele lor sunt descrise în articolul Limitarea rezonabilă a post_max_size și upload_max_filesize.
Evaluarea corectă a codurilor de eroare
Câmpul error spune ce s-a întâmplat la procesare. Constantele merg de la UPLOAD_ERR_OK (0), prin UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) și UPLOAD_ERR_NO_FILE (4), până la UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) și 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);
}
Exemplul arată doar mecanismul. Verificarea tipului de fișier, stocarea în afara webroot-ului și blocarea execuției scripturilor sunt descrise în ghidul Implementarea sigură a upload-urilor de fișiere în PHP.
Upload-uri și PHP2Shell: ce înseamnă măsurile de tranziție
Vulnerabilitățile PHP2Shell CVE-2026-109618 și CVE-2026-109619 afectează procesarea din jurul cererii în interpretorul PHP. De aceea php.net indică drept măsuri de tranziție, printre altele, post_max_size = 8M (fără upload-uri) sau 32M (cu upload-uri), upload_max_filesize sub această valoare, precum și session.upload_progress.enabled=0. Aceste valori se așază în php.ini, în pool-ul FPM sau în configurația serverului, după care toți workerii trebuie reporniți.
Autori de specialitate propun suplimentar blocarea cererilor multipart/form-data în afara căilor reale de upload, la proxy sau în WAF. Aceasta este o măsură neoficială, cu riscul de a strica formulare; analiza compromisului este descrisă în articolul Filtrarea cererilor de upload la margine. Niciuna dintre aceste măsuri nu înlocuiește actualizarea la PHP 8.5.12, 8.4.27, 8.3.36 sau 8.2.35.
Cum ajută PHP SOS concret?
- Stabilim care dintre site-urile dumneavoastră au cu adevărat nevoie de upload-uri și pe ce căi.
- Setăm limitele și măsurile de tranziție pentru fiecare pool, repornim toți workerii și testăm formularele.
- Verificăm directoarele de upload pentru fișiere executabile și blocăm execuția scripturilor în cadrul securizării serverului.
- Instalăm actualizarea PHP și documentăm starea de patch pentru fiecare site.
Întrebări frecvente
Unde stochează PHP mai întâi fișierele încărcate?
Într-un director temporar. Implicit este directorul temporar al sistemului; cu upload_tmp_dir poate fi schimbat. Dacă fișierul nu este mutat în script, PHP îl șterge la sfârșitul cererii.
Pot avea încredere în câmpul type din $_FILES?
Nu. Tipul MIME este raportat de browser, iar PHP nu îl verifică, potrivit manualului. Și name, respectiv full_path provin de la expeditor. Tipul real al fișierului îl stabiliți pe server, pe baza conținutului.
De ce este $_FILES gol, deși a fost trimis un fișier?
De obicei întreaga cerere a fost mai mare decât post_max_size. În acest caz PHP golește complet $_POST și $_FILES, fără cod de eroare în array. Alte cauze sunt lipsa enctype din formular sau file_uploads dezactivat.
Ajută împotriva PHP2Shell dacă interzic upload-urile în aplicație?
Nu. Procesarea datelor cererii are loc în PHP, înainte ca să ruleze codul aplicației. Eficiente sunt actualizarea PHP și repornirea tuturor workerilor; limitele mai stricte sunt doar o măsură de tranziție.
Care este diferența dintre upload_max_filesize și post_max_size?
upload_max_filesize limitează fiecare fișier în parte, iar post_max_size întregul conținut al cererii, inclusiv toate fișierele și câmpurile. De aceea post_max_size trebuie să fie mai mare decât upload_max_filesize.