# Kako PHP obrađuje upload datoteka

> Kako PHP interno obrađuje upload datoteka: multipart/form-data, privremene datoteke u upload_tmp_dir, polje $_FILES, ograničenja i šifre pogrešaka.

Izvor: https://phpsos.de/hr/php/datei-uploads-intern/  
Stanje: 25.10.2026  
Pružatelj: PHP SOS, PHP hitna pomoć i sigurnost poslužitelja. Hitna pomoć putem WhatsAppa: https://wa.me/491736494914?text=Pozdrav%20PHP%20SOS%2C%20trebamo%20pomo%C4%87%20s%20na%C5%A1im%20PHP%20poslu%C5%BEiteljem.%20Tvrtka%3A%20%E2%80%A6%20Web-stranica%3A%20%E2%80%A6

**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:

1. Šalje se obrazac s `method="POST"` i `enctype="multipart/form-data"`.
2. 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.
3. PHP provjerava ograničenja poput `post_max_size` i rastavlja sadržaj prema graničnom nizu znakova, takozvanom boundaryju.
4. Obična polja završavaju u `$_POST`, dijelovi s datotekama zapisuju se u privremene datoteke i opisuju u `$_FILES`.
5. Tek sada se pokreće skripta, provjerava datoteku i premješta je na odredište.
6. 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:

```http
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](https://phpsos.de/hr/blog/post-max-size-upload-limits/).

## 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
<?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](https://phpsos.de/hr/ratgeber/datei-upload-sicher/).

## 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](https://phpsos.de/hr/blog/upload-anfragen-am-rand-filtern/). 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](https://phpsos.de/hr/leistungen/server-haertung/).
- 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.

## Izvori

- [PHP priručnik: Uploadi putem POST-a](https://www.php.net/manual/de/features.file-upload.post-method.php)
- [PHP priručnik: Poruke o pogreškama pri uploadu](https://www.php.net/manual/de/features.file-upload.errors.php)
- [PHP priručnik: php.ini direktive](https://www.php.net/manual/de/ini.core.php)
- [php.net: Sigurnosne obavijesti](https://www.php.net/)
