Na kratko: Ko brskalnik naloži datoteko, pošlje zahtevo POST v obliki multipart/form-data, v kateri so polja obrazca in vsebine datotek kot posamezni deli zapisani drug za drugim. PHP to zahtevo razčleni sam, vsak del z datoteko zapiše v začasno datoteko v upload_tmp_dir, podatke vpiše v $_FILES in šele nato zažene vašo skripto. Skripta datoteko premakne z move_uploaded_file(); vse, kar ni premaknjeno, PHP ob koncu zahteve izbriše.
Pot datoteke od brskalnika do skripte
Za odločevalce je predvsem pomembno eno: nalaganje ni opravilo same aplikacije. Velik del dela se opravi v interpreterju PHP, še preden je koda sistema CMS ali spletne trgovine sploh vpletena. Zato napake v tem delu PHP zadevajo vsako spletno stran, ne glede na to, ali ima obrazec za nalaganje ali ne.
Potek na kratko:
- Odda se obrazec z
method="POST"inenctype="multipart/form-data". - Spletni strežnik zahtevo sprejme in jo preda PHP, odvisno od načina delovanja prek PHP-FPM, mod_php ali drugega vmesnika.
- PHP preveri omejitve, kot je
post_max_size, in vsebino razdeli na podlagi ločilnega niza, tako imenovane meje (boundary). - Navadna polja pristanejo v
$_POST, deli z datotekami se zapišejo v začasne datoteke in opišejo v$_FILES. - Šele zdaj se zažene skripta, preveri datoteko in jo premakne na ciljno mesto.
- Ob koncu zahteve PHP izbriše vse začasne datoteke, ki niso bile premaknjene.
multipart/form-data: zgradba zahteve za nalaganje
Zahteva za nalaganje je čisto besedilo z vdelanimi podatki datoteke. Glava Content-Type navaja mejo (boundary), v vsebini pa ta niz loči posamezne dele. Vsak del ima lastne glave z imenom polja in po potrebi imenom datoteke. Močno skrajšan, neškodljiv primer:
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--
Vsi podatki v tem besedilu izvirajo od pošiljatelja, tudi ime datoteke in Content-Type dela. PHP jih prevzame v $_FILES, vendar ne preverja njihove verodostojnosti.
Polje $_FILES
Za polje z imenom lebenslauf $_FILES['lebenslauf'] po priročniku PHP vsebuje te ključe:
| Ključ | Vsebina | Ali mu je mogoče zaupati? |
|---|---|---|
name |
prvotno ime datoteke pri pošiljatelju | ne |
full_path |
pot, ki jo sporoči brskalnik, od PHP 8.1.0 | ne |
type |
tip MIME po navedbi brskalnika, PHP ga ne preverja | ne |
size |
velikost v bajtih | ugotovi PHP |
tmp_name |
pot začasne datoteke na strežniku | dodeli PHP |
error |
koda napake pri obdelavi nalaganja | nastavi PHP |
Pri poljih z oglatimi oklepaji v imenu, na primer bilder[], vsak ključ vsebuje polje z enim vnosom na datoteko. Aplikacija nato na primer prehodi $_FILES['bilder']['error'] in prek istega indeksa dostopa do tmp_name in name.
Obrazec lahko pred poljem za datoteko dodatno pošlje skrito polje MAX_FILE_SIZE. Po priročniku je to samo namig brskalniku, ki ga je mogoče zlahka obiti. Zanesete se lahko izključno na meje, ki jih uveljavlja strežnik: v PHP, v spletnem strežniku in v sami aplikaciji.
Omejitve: direktive php.ini za nalaganje
Te direktive nadzorujejo obdelavo nalaganja. Privzete vrednosti izhajajo iz priročnika PHP in veljajo, kadar jih php.ini ne prepiše.
| Direktiva | Privzeto | Mogoče spremeniti v |
|---|---|---|
file_uploads |
1 |
samo sistemska konfiguracija |
upload_tmp_dir |
prazno (sistemski temp) | samo sistemska konfiguracija |
upload_max_filesize |
2M |
php.ini, pool, konfiguracija imenika |
post_max_size |
8M |
php.ini, pool, konfiguracija imenika |
max_file_uploads |
20 |
php.ini, pool, konfiguracija imenika |
enable_post_data_reading |
1 |
php.ini, pool, konfiguracija imenika |
post_max_size mora biti večji od upload_max_filesize, memory_limit pa mora biti po priročniku praviloma večji od post_max_size. Če v upload_tmp_dir ni mogoče pisati, PHP preide na temp imenik sistema. Pri open_basedir mora biti temp imenik dovoljen, sicer nalaganje ne uspe. Praktične vrednosti in njihove posledice opisuje prispevek Smiselna omejitev post_max_size in upload_max_filesize.
Pravilno ovrednotenje kod napak
Polje error pove, kaj se je zgodilo pri obdelavi. Konstante segajo od UPLOAD_ERR_OK (0) prek UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) in UPLOAD_ERR_NO_FILE (4) do UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) in 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);
}
Primer kaže samo mehaniko. Preverjanje tipa datoteke, shranjevanje zunaj korena spleta (webroot) in onemogočanje izvajanja skript opisuje vodnik Varno nalaganje datotek v PHP.
Nalaganje in PHP2Shell: kaj pomenijo prehodni ukrepi
Ranljivosti PHP2Shell CVE-2026-109618 in CVE-2026-109619 zadevata obdelavo okoli zahteve v interpreterju PHP. Zato php.net kot prehodne ukrepe med drugim navaja post_max_size = 8M (brez nalaganja) ali 32M (z nalaganjem), upload_max_filesize pod tem, pa tudi session.upload_progress.enabled=0. Te vrednosti sodijo v php.ini, pool FPM ali konfiguracijo strežnika, nato pa je treba znova zagnati vse workerje.
Strokovni avtorji dodatno predlagajo, da se zahteve multipart/form-data zunaj pravih poti za nalaganje blokirajo na proxyju ali v WAF. To je neuradni ukrep s tveganjem, da se obrazci pokvarijo; tehtanje opisuje prispevek Filtriranje zahtev za nalaganje na robu. Noben od teh ukrepov ne nadomesti posodobitve na PHP 8.5.12, 8.4.27, 8.3.36 ali 8.2.35.
Kako PHP SOS pomaga konkretno?
- Ugotovimo, katere vaše spletne strani dejansko potrebujejo nalaganje in na katerih poteh.
- Omejitve in prehodne ukrepe nastavimo po poolih, znova zaženemo vse workerje in preizkusimo obrazce.
- Imenike za nalaganje preverimo glede izvršljivih datotek in onemogočimo izvajanje skript v okviru utrjevanja strežnika.
- Namestimo posodobitev PHP in dokumentiramo stanje popravkov po posameznih spletnih straneh.
Pogosta vprašanja
Kam PHP najprej shrani naložene datoteke?
V začasen imenik. Privzeto je to začasni imenik sistema; z upload_tmp_dir ga je mogoče spremeniti. Če skripta datoteke ne premakne, jo PHP izbriše ob koncu zahteve.
Ali lahko zaupam polju type v $_FILES?
Ne. Tip MIME sporoči brskalnik, PHP ga po priročniku ne preverja. Tudi name in full_path izvirata od pošiljatelja. Dejanski tip datoteke ugotovite na strežniku na podlagi vsebine.
Zakaj je $_FILES prazen, čeprav je bila datoteka poslana?
Najpogosteje je bila celotna zahteva večja od post_max_size. Takrat PHP popolnoma izprazni $_POST in $_FILES, brez kode napake v polju. Drugi vzroki so manjkajoč enctype v obrazcu ali izklopljen file_uploads.
Ali proti PHP2Shell pomaga, če v aplikaciji prepovem nalaganje?
Ne. Obdelava podatkov zahteve poteka v PHP, še preden se izvede koda aplikacije. Učinkovita sta posodobitev PHP in ponovni zagon vseh workerjev; ožje omejitve so samo prehodni ukrep.
Kakšna je razlika med upload_max_filesize in post_max_size?
upload_max_filesize omejuje vsako posamezno datoteko, post_max_size pa celotno vsebino zahteve, vključno z vsemi datotekami in polji. post_max_size mora zato biti večji od upload_max_filesize.