Ukratko: Kad preglednik učitava datoteku, šalje POST zahtjev u formatu multipart/form-data, u kojem se polja obrasca i sadržaji datoteka nižu kao zasebni dijelovi. PHP sam rastavlja taj zahtjev, svaki dio s datotekom zapisuje u privremenu datoteku u upload_tmp_dir, unosi podatke u $_FILES i tek tada pokreće Vašu skriptu. Skripta datoteku premješta funkcijom move_uploaded_file(); sve što se ne premjesti, PHP briše na kraju zahtjeva.
Put datoteke od preglednika do skripte
Za donositelje odluka ponajprije je važno jedno: upload nije postupak same aplikacije. Velik dio posla obavlja se u PHP interpreteru prije nego što kod CMS-a ili shopa uopće sudjeluje. Zato pogreške u tom dijelu PHP-a pogađaju svaku web-stranicu, imala ona obrazac za upload ili ne.
Pregled tijeka:
- Šalje se obrazac s
method="POST"ienctype="multipart/form-data". - Web-poslužitelj prihvaća zahtjev i prosljeđuje ga PHP-u, ovisno o načinu rada putem PHP-FPM-a, mod_php-a ili drugog sučelja.
- PHP provjerava ograničenja poput
post_max_sizei rastavlja sadržaj prema graničnom nizu znakova, takozvanom boundaryju. - Obična polja završavaju u
$_POST, dijelovi s datotekama zapisuju se u privremene datoteke i opisuju u$_FILES. - Tek sada se pokreće skripta, provjerava datoteku i premješta je na odredište.
- Na kraju zahtjeva PHP briše sve privremene datoteke koje nisu premještene.
multipart/form-data: struktura zahtjeva za upload
Zahtjev za upload je običan tekst s ugrađenim podacima datoteke. Zaglavlje Content-Type navodi boundary, a u sadržaju taj niz znakova razdvaja pojedine dijelove. Svaki dio ima vlastita zaglavlja s nazivom polja i, po potrebi, nazivom datoteke. Jako skraćen, bezopasan primjer:
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--
Svi podaci u tom tekstu potječu od pošiljatelja, uključujući naziv datoteke i Content-Type dijela. PHP ih preuzima u $_FILES, ali ne provjerava njihovu vjerodostojnost.
Polje $_FILES
Za polje naziva lebenslauf $_FILES['lebenslauf'] prema PHP priručniku sadrži ove ključeve:
| Ključ | Sadržaj | Pouzdano? |
|---|---|---|
name |
izvorni naziv datoteke kod pošiljatelja | ne |
full_path |
putanja koju je javio preglednik, od PHP-a 8.1.0 | ne |
type |
MIME tip prema pregledniku, PHP ga ne provjerava | ne |
size |
veličina u bajtovima | utvrđuje PHP |
tmp_name |
putanja privremene datoteke na poslužitelju | dodjeljuje PHP |
error |
šifra pogreške pri obradi uploada | postavlja PHP |
Kod polja s uglatim zagradama u nazivu, primjerice bilder[], svaki ključ sadrži polje s jednim unosom po datoteci. Aplikacija tada prolazi, na primjer, kroz $_FILES['bilder']['error'] i istim indeksom pristupa ključevima tmp_name i name.
Obrazac dodatno može poslati skriveno polje MAX_FILE_SIZE ispred polja za datoteku. Prema priručniku to je samo savjet pregledniku koji se lako zaobilazi. Pouzdati se možete isključivo u granice koje provodi poslužitelj: u PHP-u, u web-poslužitelju i u samoj aplikaciji.
Ograničenja: php.ini direktive za uploade
Te direktive upravljaju obradom uploada. Zadane vrijednosti potječu iz PHP priručnika i vrijede ako ih nijedan php.ini ne nadjača.
| Direktiva | Zadano | Promjenjivo u |
|---|---|---|
file_uploads |
1 |
samo sistemska konfiguracija |
upload_tmp_dir |
prazno (privremeni direktorij sustava) | samo sistemska konfiguracija |
upload_max_filesize |
2M |
php.ini, pool, konfiguracija direktorija |
post_max_size |
8M |
php.ini, pool, konfiguracija direktorija |
max_file_uploads |
20 |
php.ini, pool, konfiguracija direktorija |
enable_post_data_reading |
1 |
php.ini, pool, konfiguracija direktorija |
post_max_size mora biti veći od upload_max_filesize, a memory_limit bi prema priručniku u pravilu trebao biti veći od post_max_size. Ako u upload_tmp_dir nije moguće pisati, PHP prelazi na privremeni direktorij sustava. S open_basedir privremeni direktorij mora biti dopušten, inače uploadi ne uspijevaju. Praktične vrijednosti i njihove posljedice opisuje članak Smisleno ograničavanje post_max_size i upload_max_filesize.
Ispravno vrednovati šifre pogrešaka
Polje error govori što se dogodilo pri obradi. Konstante idu od UPLOAD_ERR_OK (0) preko UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) i UPLOAD_ERR_NO_FILE (4) do 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);
}
Primjer prikazuje samo mehaniku. Provjeru vrste datoteke, pohranu izvan webrootea i blokiranje izvršavanja skripti opisuje vodič Sigurna izvedba uploada datoteka u PHP-u.
Uploadi i PHP2Shell: što znače prijelazne mjere
PHP2Shell ranjivosti CVE-2026-109618 i CVE-2026-109619 tiču se obrade oko zahtjeva u PHP interpreteru. Zato php.net kao prijelazne mjere navodi, među ostalim, post_max_size = 8M (bez uploada) odnosno 32M (s uploadima), upload_max_filesize ispod toga te session.upload_progress.enabled=0. Te vrijednosti pripadaju u php.ini, FPM pool ili konfiguraciju poslužitelja, a nakon toga svi workeri moraju se ponovno pokrenuti.
Stručni autori dodatno predlažu blokiranje multipart/form-data zahtjeva izvan stvarnih putanja za upload na proxyju ili u WAF-u. To je neslužbena mjera s rizikom da obrasci prestanu raditi; odvagivanje opisuje članak Filtriranje zahtjeva za upload na rubu mreže. Nijedna od tih mjera ne zamjenjuje ažuriranje na PHP 8.5.12, 8.4.27, 8.3.36 ili 8.2.35.
Kako PHP SOS konkretno pomaže?
- Utvrđujemo koje Vaše web-stranice stvarno trebaju uploade i na kojim putanjama.
- Postavljamo ograničenja i prijelazne mjere po poolu, ponovno pokrećemo sve workere i testiramo obrasce.
- Provjeravamo direktorije za upload na izvršne datoteke i blokiramo izvršavanje skripti u okviru ojačavanja poslužitelja.
- Instaliramo PHP ažuriranje i dokumentiramo stanje zakrpa po web-stranici.
Česta pitanja
Gdje PHP najprije sprema učitane datoteke?
U privremeni direktorij. Prema zadanim postavkama to je privremeni direktorij sustava, a s upload_tmp_dir može se promijeniti. Ako se datoteka u skripti ne premjesti, PHP je briše na kraju zahtjeva.
Smijem li vjerovati polju type u $_FILES?
Ne. MIME tip javlja preglednik, a PHP ga prema priručniku ne provjerava. I name i full_path potječu od pošiljatelja. Stvarnu vrstu datoteke utvrdite na poslužitelju na temelju sadržaja.
Zašto je $_FILES prazan iako je datoteka poslana?
Najčešće je cijeli zahtjev bio veći od post_max_size. Tada PHP potpuno prazni $_POST i $_FILES, bez šifre pogreške u polju. Daljnji uzroci su nedostajući enctype u obrascu ili isključeni file_uploads.
Pomaže li protiv PHP2Shella zabrana uploada u aplikaciji?
Ne. Obrada podataka zahtjeva odvija se u PHP-u prije nego što se izvrši kod aplikacije. Učinkovito je PHP ažuriranje i ponovno pokretanje svih workera; stroža ograničenja samo su prijelazna mjera.
Koja je razlika između upload_max_filesize i post_max_size?
upload_max_filesize ograničava svaku pojedinu datoteku, a post_max_size cjelokupni sadržaj zahtjeva uključujući sve datoteke i polja. post_max_size zato mora biti veći od upload_max_filesize.